Compare commits

...

12 commits

Author SHA1 Message Date
e562817597 Merge branch 'main' into rm-rsa-keys 2025-02-20 12:43:12 +01:00
586c3b851a Merge pull request 'convert readmes from org to markdown' (#166) from kiara/Fediversity:org-to-md into main
Reviewed-on: Fediversity/Fediversity#166
Reviewed-by: Nicolas Jeannerod <nicolas.jeannerod@moduscreate.com>
2025-02-20 12:42:54 +01:00
c2db12a735 add simple (ngi) favicon to fedi panel, fixes 404 not found error (#167)
Reviewed-on: Fediversity/Fediversity#167
Reviewed-by: Valentin Gagarin <valentin.gagarin@tweag.io>
Co-authored-by: Kiara Grouwstra <kiara@procolix.eu>
Co-committed-by: Kiara Grouwstra <kiara@procolix.eu>
2025-02-19 23:10:33 +01:00
57d53a1d22 Make trim-trailing-whitespace hook apply everywhere 2025-02-19 23:08:19 +01:00
740b5447d8 Remove trailing whitespace everywhere 2025-02-19 23:08:19 +01:00
142af8d0ee Make pre-commits opt-out instead of opt-in 2025-02-19 23:08:19 +01:00
3ec09b491d matrix: opt-in to pre-commits 2025-02-19 23:08:19 +01:00
01de49d096 website: opt-in to pre-commits 2025-02-19 23:08:19 +01:00
06d3d37a39 website: remove unused arguments 2025-02-19 23:08:19 +01:00
10f3d15a98 website: format 2025-02-19 23:08:19 +01:00
92563d387a test login/logout redirection (#163)
this concludes Fediversity/Fediversity#72 with a test covering most of the user story.

test in the devshell:
```
manage test panel
```

test in full isolation:
```
nix-build -A tests
```

Reviewed-on: Fediversity/Fediversity#163
Reviewed-by: kiara Grouwstra <kiara@procolix.eu>
2025-02-19 23:07:51 +01:00
fb64d2b9c9
convert readmes from org to markdown 2025-02-19 20:23:48 +01:00
59 changed files with 2189 additions and 1566 deletions

View file

@ -94,11 +94,11 @@ Not everyone has the expertise and time to run their own server.
- Resource - Resource
A [resource for NixOps4](https://nixops.dev/manual/development/concept/resource.html) is any external entity that can be declared with NixOps4 expressions and manipulated with NixOps4, such as a virtual machine, an active NixOS configuration, a DNS entry, or customer database. A [resource for NixOps4](https://nixops.dev/manual/development/concept/resource.html) is any external entity that can be declared with NixOps4 expressions and manipulated with NixOps4, such as a virtual machine, an active NixOS configuration, a DNS entry, or customer database.
- Resource provider - Resource provider
A resource provider for NixOps4 is an executable that communicates between a resource and NixOps4 using a standardised protocol, allowing [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) on the resources to be performed by NixOps4. A resource provider for NixOps4 is an executable that communicates between a resource and NixOps4 using a standardised protocol, allowing [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) on the resources to be performed by NixOps4.
Refer to the [NixOps4 manual](https://nixops.dev/manual/development/resource-provider/index.html) for details. Refer to the [NixOps4 manual](https://nixops.dev/manual/development/resource-provider/index.html) for details.
> Example: We need a resource provider for obtaining deployment secrets from a database. > Example: We need a resource provider for obtaining deployment secrets from a database.

219
deployment/README.md Normal file
View file

@ -0,0 +1,219 @@
# Provisioning VMs via Proxmox
## Quick links
Proxmox API doc
: <https://pve.proxmox.com/pve-docs/api-viewer>
Fediversity Proxmox
: <http://192.168.51.81:8006/>
## Basic terminology
Node
: physical host
## Fediversity Proxmox
- It is only accessible via Procolix\'s VPN:
- Get credentials for the VPN portal and Proxmox from
[Kevin](https://git.fediversity.eu/kevin).
- Log in to the [VPN
portal](https://vpn.fediversity.eu/vpn-user-portal/home).
- Create a **New Configuration**:
- Select **WireGuard (UDP)**
- Enter some name, e.g. `fediversity`
- Click Download
- Write the WireGuard configuration to a file
`fediversity-vpn.config` next to your NixOS configuration
- Add that file's path to `.git/info/exclude` and make sure
it doesn't otherwise leak (for example, use
[Agenix](https://github.com/ryantm/agenix) to manage
secrets)
- To your NixOS configuration, add
``` nix
networking.wg-quick.interfaces.fediversity.configFile = toString ./fediversity-vpn.config;
```
- Select "Promox VE authentication server".
- Ignore the "You do not have a valid subscription" message.
## Automatically
This directory contains scripts that can automatically provision or
remove a Proxmox VM. For now, they are tied to one node in the
Fediversity Proxmox, but it would not be difficult to make them more
generic. Try:
```sh
bash proxmox/provision.sh --help
bash proxmox/remove.sh --help
```
## Preparing the machine configuration
- It is nicer if the machine is a QEMU guest. On NixOS:
``` nix
services.qemuGuest.enable = true
```
- Choose name for your machine.
- Choose static IPs for your machine. The IPv4 and IPv6 subnets
available for Fediversity testing are:
- `95.215.187.0/24`. Gateway is `95.215.187.1`.
- `2a00:51c0:13:1305::/64`. Gateway is `2a00:51c0:13:1305::1`.
- I have been using id `XXX` (starting from `001`), name `fediXXX`,
`95.215.187.XXX` and `2a00:51c0:13:1305::XXX`.
- Name servers should be `95.215.185.6` and `95.215.185.7`.
- Check [Netbox](https://netbox.protagio.org) to see which addresses
are free.
## Manually via the GUI
### Upload your ISO
- Go to Fediversity proxmox.
- In the left view, expand under the node that you want and click on
"local".
- Select "ISO Images", then click "Upload".
- Note: You can also download from URL.
- Note: You should click on "local" and not "local-zfs".
### Creating the VM
- Click "Create VM" at the top right corner.
#### General
Node
: which node will host the VM; has to be the same
VM ID
: Has to be unique, probably best to use the `xxxx` in `vm0xxxx`
(yet to be decided)
Name
: Usually `vm` + 5 digits, e.g. `vm02199`
Resource pool
: Fediversity
#### OS
Use CD/DVD disc image file (iso)
:
Storage
: local, means storage of the node.
ISO image
: select the image previously uploaded
No need to touch anything else
#### System
BIOS
: OVMF (UEFI)
EFI Storage
: `linstor_storage`; this is a storage shared by all of the Proxmox
machines.
Pre-Enroll keys
: MUST be unchecked
Qemu Agent
: check
#### Disks
- Tick "advanced" at the bottom.
- Disk size (GiB) :: 40 (depending on requirements)
- SSD emulation :: check (only visible if "Advanced" is checked)
- Discard :: check, so that blocks of removed data are cleared
#### CPU
Sockets
: 1 (depending on requirements)
Cores
: 2 (depending on requirements)
Enable NUMA
: check
#### Memory
Memory (MiB)
: choose what you want
Ballooning Device
: leave checked (only visible if "Advanced" is checked)
#### Network
Bridge
: `vnet1306`. This is the provisioning bridge;
we will change it later.
Firewall
: uncheck, we will handle the firewall on the VM itself
#### Confirm
### Install and start the VM
- Start the VM a first time.
- Select the VM in the left panel. You might have to expand the
node on which it is hosted.
- Select "Console" and start the VM.
- Install the VM as you would any other machine.
- [*Shutdown the VM*]{.spurious-link target="Shutdown the VM"}.
- After the VM has been installed:
- Select the VM again, then go to "Hardware".
- Double click on the CD/DVD Drive line. Select "Do not use any
media" and press OK.
- Double click on Network Device, and change the bridge to
`vnet1305`, the public bridge.
- Start the VM again.
### Remove the VM
- [*Shutdown the VM*]{.spurious-link target="Shutdown the VM"}.
- On the top right corner, click "More", then "Remove".
- Enter the ID of the machine.
- Check "Purge from job configurations"
- Check "Destroy unreferenced disks owned by guest"
- Click "Remove".
### Move the VM to another node
- Make sure there is no ISO plugged in.
- Click on the VM. Click migrate. Choose target node. Go.
- Since the storage is shared, it should go pretty fast (~1 minute).
### Shutdown the VM
- Find the VM in the left panel.
- At the top right corner appears a "Shutdown" button with a submenu.
- Clicking "Shutdown" sends a signal to shutdown the machine. This
might not work if the machine is not listening for that signal.
- Brutal solution: in the submenu, select "Stop".
- The checkbox "Overrule active shutdown tasks" means that the machine
should be stopped even if a shutdown is currently ongoing. This is
particularly important if you have tried to shut the machine down
normally just before.

View file

@ -1,113 +0,0 @@
#+title: Provisioning VMs via Proxmox
* Quick links
- Proxmox API doc :: https://pve.proxmox.com/pve-docs/api-viewer
- Fediversity Proxmox :: http://192.168.51.81:8006/
* Basic terminology
- Node :: physical host
* Fediversity Proxmox
- It is only accessible via Procolix's VPN:
- Get credentials for the VPN portal and Proxmox from [[https://git.fediversity.eu/kevin][Kevin]].
- Log in to the [[https://vpn.fediversity.eu/vpn-user-portal/home][VPN portal]].
- Create a *New Configuration*:
- Select *WireGuard (UDP)*
- Enter some name, e.g. ~fediversity~
- Click Download
- Write the WireGuard configuration to a file ~fediversity-vpn.config~ next to your NixOS configuration
- Add that file's path to ~.git/info/exclude~ and make sure it doesn't otherwise leak (for example, use [[https://github.com/ryantm/agenix][Agenix]] to manage secrets)
- To your NixOS configuration, add
#+begin_src nix
networking.wg-quick.interfaces.fediversity.configFile = toString ./fediversity-vpn.config;
#+end_src
- Select “Promox VE authentication server”.
- Ignore the “You do not have a valid subscription” message.
* Automatically
This directory contains scripts that can automatically provision or remove a
Proxmox VM. For now, they are tied to one node in the Fediversity Proxmox, but
it would not be difficult to make them more generic. Try:
#+begin_src sh
sh proxmox/provision.sh --help
sh proxmox/remove.sh --help
#+end_src
* Preparing the machine configuration
- It is nicer if the machine is a QEMU guest. On NixOS:
#+begin_src nix
services.qemuGuest.enable = true
#+end_src
- Choose name for your machine.
- Choose static IPs for your machine. The IPv4 and IPv6 subnets available for
Fediversity testing are:
- ~95.215.187.0/24~. Gateway is ~95.215.187.1~.
- ~2a00:51c0:13:1305::/64~. Gateway is ~2a00:51c0:13:1305::1~.
- I have been using id ~XXX~ (starting from ~001~), name ~fediXXX~, ~95.215.187.XXX~ and
~2a00:51c0:13:1305::XXX~.
- Name servers should be ~95.215.185.6~ and ~95.215.185.7~.
- Check [[https://netbox.protagio.org][Netbox]] to see which addresses are free.
* Manually via the GUI
** Upload your ISO
- Go to Fediversity proxmox.
- In the left view, expand under the node that you want and click on “local”.
- Select “ISO Images”, then click “Upload”.
- Note: You can also download from URL.
- Note: You should click on “local” and not “local-zfs”.
** Creating the VM
- Click “Create VM” at the top right corner.
*** General
- Node :: which node will host the VM; has to be the same
- VM ID :: Has to be unique, probably best to use the "xxxx" in "vm0xxxx" (yet to be decided)
- Name :: Usually "vm" + 5 digits, e.g. "vm02199"
- Resource pool :: Fediversity
*** OS
- Use CD/DVD disc image file (iso) ::
- Storage :: local, means storage of the node.
- ISO image :: select the image previously uploaded
No need to touch anything else
*** System
- BIOS :: OVMF (UEFI)
- EFI Storage :: ~linstor_storage~; this is a storage shared by all of the Proxmox machines.
- Pre-Enroll keys :: MUST be unchecked
- Qemu Agent :: check
*** Disks
- Tick “advanced” at the bottom.
- Disk size (GiB) :: 40 (depending on requirements)
- SSD emulation :: check (only visible if “Advanced” is checked)
- Discard :: check, so that blocks of removed data are cleared
*** CPU
- Sockets :: 1 (depending on requirements)
- Cores :: 2 (depending on requirements)
- Enable NUMA :: check
*** Memory
- Memory (MiB) :: choose what you want
- Ballooning Device :: leave checked (only visible if “Advanced” is checked)
*** Network
- Bridge :: ~vnet1306~. This is the provisioning bridge; we will change it later.
- Firewall :: uncheck, we will handle the firewall on the VM itself
*** Confirm
** Install and start the VM
- Start the VM a first time.
- Select the VM in the left panel. You might have to expand the node on which it is hosted.
- Select “Console” and start the VM.
- Install the VM as you would any other machine.
- [[Shutdown the VM]].
- After the VM has been installed:
- Select the VM again, then go to “Hardware”.
- Double click on the CD/DVD Drive line. Select “Do not use any media” and press OK.
- Double click on Network Device, and change the bridge to ~vnet1305~, the public bridge.
- Start the VM again.
** Remove the VM
- [[Shutdown the VM]].
- On the top right corner, click “More”, then “Remove”.
- Enter the ID of the machine.
- Check “Purge from job configurations”
- Check “Destroy unreferenced disks owned by guest”
- Click “Remove”.
** Move the VM to another node
- Make sure there is no ISO plugged in.
- Click on the VM. Click migrate. Choose target node. Go.
- Since the storage is shared, it should go pretty fast (~1 minute).
** Shutdown the VM
- Find the VM in the left panel.
- At the top right corner appears a “Shutdown” button with a submenu.
- Clicking “Shutdown” sends a signal to shutdown the machine. This might not work if the machine is not listening for that signal.
- Brutal solution: in the submenu, select “Stop”.
- The checkbox “Overrule active shutdown tasks” means that the machine should be stopped even if a shutdown is currently ongoing. This is particularly important if you have tried to shut the machine down normally just before.

View file

@ -41,32 +41,23 @@
formatter = pkgs.nixfmt-rfc-style; formatter = pkgs.nixfmt-rfc-style;
pre-commit.settings.hooks = pre-commit.settings.hooks =
## Not everybody might want pre-commit hooks, so we make them
## opt-in. Maybe one day we will decide to have them everywhere.
let let
inherit (builtins) concatStringsSep; ## Add a directory here if pre-commit hooks shouldn't apply to it.
optin = [ optout = [ "npins" ];
"deployment" excludes = map (dir: "^${dir}/") optout;
"infra"
"keys"
"secrets"
"services"
"panel"
];
files = "^((" + concatStringsSep "|" optin + ")/.*\\.nix|[^/]*\\.nix)$";
in in
{ {
nixfmt-rfc-style = { nixfmt-rfc-style = {
enable = true; enable = true;
inherit files; inherit excludes;
}; };
deadnix = { deadnix = {
enable = true; enable = true;
inherit files; inherit excludes;
}; };
trim-trailing-whitespace = { trim-trailing-whitespace = {
enable = true; enable = true;
inherit files; inherit excludes;
}; };
}; };

65
infra/README.md Normal file
View file

@ -0,0 +1,65 @@
# Infra
This directory contains the definition of the VMs that host our infrastructure.
## NixOps4
Their configuration can be updated via NixOps4. Run
```sh
nixops4 deployments list
```
to see the available deployments.
This should be done from the root of the repository,
otherwise NixOps4 will fail with something like:
```
nixops4 error: evaluation: error:
… while calling the 'getFlake' builtin
error: path '/nix/store/05nn7krhvi8wkcyl6bsysznlv60g5rrf-source/flake.nix' does not exist, evaluation: error:
… while calling the 'getFlake' builtin
error: path '/nix/store/05nn7krhvi8wkcyl6bsysznlv60g5rrf-source/flake.nix' does not exist
```
Then, given a deployment (eg. `git`), run
```sh
nixops4 apply <deployment>
```
Alternatively, to run the `default` deployment, run
```sh
nixops4 apply
```
## Deployments
default
: Contains everything
`git`
: Machines hosting our Git infrastructure, eg. Forgejo and its actions runners
`web`
: Machines hosting our online content, eg. the website or the wiki
`other`
: Machines without a specific purpose
## Machines
These machines are hosted on the Procolix Proxmox instance,
to which non-Procolix members of the project do not have access.
They host our stable infrastructure.
Machine Proxmox Description Deployment
--------- ------------- ------------------------ ------------
vm02116 Procolix Forgejo `git`
vm02179 Procolix *unused* `other`
vm02186 Procolix *unused* `other`
vm02187 Procolix Wiki `web`
fedi300 Fediversity Forgejo actions runner `git`

View file

@ -1,58 +0,0 @@
#+title: Infra
This directory contains the definition of the VMs that host our infrastructure.
* NixOps4
Their configuration can be updated via NixOps4. Run
#+begin_src sh
nixops4 deployments list
#+end_src
to see the available deployments. This should be done from the root of the
repository, otherwise NixOps4 will fail with something like:
#+begin_src
nixops4 error: evaluation: error:
… while calling the 'getFlake' builtin
error: path '/nix/store/05nn7krhvi8wkcyl6bsysznlv60g5rrf-source/flake.nix' does not exist, evaluation: error:
… while calling the 'getFlake' builtin
error: path '/nix/store/05nn7krhvi8wkcyl6bsysznlv60g5rrf-source/flake.nix' does not exist
#+end_src
Then, given a deployment (eg. ~git~), run
#+begin_src sh
nixops4 apply <deployment>
#+end_src
Alternatively, to run the ~default~ deployment, run
#+begin_src sh
nixops4 apply
#+end_src
* Deployments
- default :: Contains everything
- ~git~ :: Machines hosting our Git infrastructure, eg. Forgejo and its actions
runners
- ~web~ :: Machines hosting our online content, eg. the website or the wiki
- ~other~ :: Machines without a specific purpose
* Machines
These machines are hosted on the Procolix Proxmox instance, to which
non-Procolix members of the project do not have access. They host our stable
infrastructure.
| Machine | Proxmox | Description | Deployment |
|---------+-------------+------------------------+------------|
| vm02116 | Procolix | Forgejo | ~git~ |
| vm02179 | Procolix | /unused/ | ~other~ |
| vm02186 | Procolix | /unused/ | ~other~ |
| vm02187 | Procolix | Wiki | ~web~ |
| fedi300 | Fediversity | Forgejo actions runner | ~git~ |

View file

@ -46,7 +46,7 @@ These are the components we're going to use:
## Synapse ## Synapse
This is the core component: the Matrix server itself, you should probably This is the core component: the Matrix server itself, you should probably
install this first. install this first.
Because not every usecase is the same, we'll describe two different Because not every usecase is the same, we'll describe two different
architectures: architectures:

View file

@ -78,10 +78,10 @@ denied-peer-ip=203.0.113.0-203.0.113.255
# TURN server allocates address family according TURN client requested address family. # TURN server allocates address family according TURN client requested address family.
# If address family not requested explicitly by the client, then it falls back to this default. # If address family not requested explicitly by the client, then it falls back to this default.
# The standard RFC explicitly define that this default must be IPv4, # The standard RFC explicitly define that this default must be IPv4,
# so use other option values with care! # so use other option values with care!
# Possible values: "ipv4" or "ipv6" or "keep" # Possible values: "ipv4" or "ipv6" or "keep"
# "keep" sets the allocation default address family according to # "keep" sets the allocation default address family according to
# the TURN client allocation request connection address family. # the TURN client allocation request connection address family.
allocation-default-address-family="ipv4" allocation-default-address-family="ipv4"

View file

@ -86,7 +86,7 @@ nginx to forward requests for reports to Draupnir:
location ~ ^/_matrix/client/(r0|v3)/rooms/([^/]*)/report/(.*)$ { location ~ ^/_matrix/client/(r0|v3)/rooms/([^/]*)/report/(.*)$ {
# The r0 endpoint is deprecated but still used by many clients. # The r0 endpoint is deprecated but still used by many clients.
# As of this writing, the v3 endpoint is the up-to-date version. # As of this writing, the v3 endpoint is the up-to-date version.
# Alias the regexps, to ensure that they're not rewritten. # Alias the regexps, to ensure that they're not rewritten.
set $room_id $2; set $room_id $2;
set $event_id $3; set $event_id $3;
@ -101,7 +101,7 @@ location /_synapse/admin/v1/event_reports {
proxy_set_header Host $host; proxy_set_header Host $host;
client_max_body_size 50M; client_max_body_size 50M;
proxy_http_version 1.1; proxy_http_version 1.1;
location ~ ^/_synapse/admin/v1/rooms/([^/]*)/context/(.*)$ { location ~ ^/_synapse/admin/v1/rooms/([^/]*)/context/(.*)$ {
set $room_id $2; set $room_id $2;
set $event_id $3; set $event_id $3;

View file

@ -308,7 +308,7 @@ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
Exit and login again to set some environment variables (yes, the installation Exit and login again to set some environment variables (yes, the installation
changes .bashrc). Then install and upgrade: changes .bashrc). Then install and upgrade:
``` ```
nvm install 23 nvm install 23
sudo apt install yarnpkg sudo apt install yarnpkg

View file

@ -187,14 +187,14 @@ server {
listen [::]:80; listen [::]:80;
listen 443 ssl http2; listen 443 ssl http2;
listen [::]:443 ssl http2; listen [::]:443 ssl http2;
ssl_certificate /etc/letsencrypt/live/element.example.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/element.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/element.example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/element.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/ssl/dhparams.pem; ssl_dhparam /etc/ssl/dhparams.pem;
server_name element.example.com; server_name element.example.com;
location / { location / {
if ($scheme = http) { if ($scheme = http) {
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
@ -204,10 +204,10 @@ server {
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "frame-ancestors 'self'"; add_header Content-Security-Policy "frame-ancestors 'self'";
} }
root /usr/share/element-web; root /usr/share/element-web;
index index.html; index index.html;
access_log /var/log/nginx/elementweb-access.log; access_log /var/log/nginx/elementweb-access.log;
error_log /var/log/nginx/elementweb-error.log; error_log /var/log/nginx/elementweb-error.log;
} }
@ -225,16 +225,16 @@ another vhost, something like this:
server { server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/ssl/dhparams.pem; ssl_dhparam /etc/ssl/dhparams.pem;
server_name admin.example.com; server_name admin.example.com;
root /var/www/synapse-admin; root /var/www/synapse-admin;
access_log /var/log/nginx/admin-access.log; access_log /var/log/nginx/admin-access.log;
error_log /var/log/nginx/admin-error.log; error_log /var/log/nginx/admin-error.log;
} }
@ -256,7 +256,7 @@ location ~ ^/_synapse/admin {
allow 111.222.111.222; allow 111.222.111.222;
allow dead:beef::/64; allow dead:beef::/64;
deny all; deny all;
proxy_pass http://localhost:8008; proxy_pass http://localhost:8008;
proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
@ -281,14 +281,14 @@ Then create a virtual host much like this:
server { server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/livekit.example.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/livekit.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/livekit.example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/livekit.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/ssl/dhparams.pem; ssl_dhparam /etc/ssl/dhparams.pem;
server_name livekit.example.com; server_name livekit.example.com;
# This is lk-jwt-service # This is lk-jwt-service
location ~ ^(/sfu/get|/healthz) { location ~ ^(/sfu/get|/healthz) {
proxy_pass http://[::1]:8080; proxy_pass http://[::1]:8080;
@ -298,19 +298,19 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location / { location / {
proxy_pass http://[::1]:7880; proxy_pass http://[::1]:7880;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
access_log /var/log/nginx/livekit-access.log; access_log /var/log/nginx/livekit-access.log;
error_log /var/log/nginx/livekit-error.log; error_log /var/log/nginx/livekit-error.log;
} }
@ -326,34 +326,34 @@ should be the configuration to publish that:
server { server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/call.example.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/call.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/call.example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/call.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/ssl/dhparams.pem; ssl_dhparam /etc/ssl/dhparams.pem;
server_name call.example.com; server_name call.example.com;
root /var/www/element-call; root /var/www/element-call;
location /assets { location /assets {
add_header Cache-Control "public, immutable, max-age=31536000"; add_header Cache-Control "public, immutable, max-age=31536000";
} }
location /apple-app-site-association { location /apple-app-site-association {
default_type application/json; default_type application/json;
} }
location /^config.json$ { location /^config.json$ {
alias public/config.json; alias public/config.json;
default_type application/json; default_type application/json;
} }
location / { location / {
try_files $uri /$uri /index.html; try_files $uri /$uri /index.html;
add_header Cache-Control "public, max-age=30, stale-while-revalidate=30"; add_header Cache-Control "public, max-age=30, stale-while-revalidate=30";
} }
access_log /var/log/nginx/call-access.log; access_log /var/log/nginx/call-access.log;
error_log /var/log/nginx/call-error.log; error_log /var/log/nginx/call-error.log;
} }

View file

@ -1,34 +1,34 @@
server { server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/call.example.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/call.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/call.example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/call.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/ssl/dhparams.pem; ssl_dhparam /etc/ssl/dhparams.pem;
server_name call.example.com; server_name call.example.com;
root /var/www/element-call; root /var/www/element-call;
location /assets { location /assets {
add_header Cache-Control "public, immutable, max-age=31536000"; add_header Cache-Control "public, immutable, max-age=31536000";
} }
location /apple-app-site-association { location /apple-app-site-association {
default_type application/json; default_type application/json;
} }
location /^config.json$ { location /^config.json$ {
alias public/config.json; alias public/config.json;
default_type application/json; default_type application/json;
} }
location / { location / {
try_files $uri /$uri /index.html; try_files $uri /$uri /index.html;
add_header Cache-Control "public, max-age=30, stale-while-revalidate=30"; add_header Cache-Control "public, max-age=30, stale-while-revalidate=30";
} }
access_log /var/log/nginx/call-access.log; access_log /var/log/nginx/call-access.log;
error_log /var/log/nginx/call-error.log; error_log /var/log/nginx/call-error.log;
} }

View file

@ -3,14 +3,14 @@ server {
listen [::]:80; listen [::]:80;
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/ssl/dhparams.pem; ssl_dhparam /etc/ssl/dhparams.pem;
server_name example.com; server_name example.com;
location /.well-known/matrix/client { location /.well-known/matrix/client {
return 200 '{ return 200 '{
"m.homeserver": {"base_url": "https://matrix.example.com"}, "m.homeserver": {"base_url": "https://matrix.example.com"},
@ -23,7 +23,7 @@ server {
default_type application/json; default_type application/json;
add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Origin' '*';
} }
location /.well-known/matrix/server { location /.well-known/matrix/server {
return 200 '{"m.server": "matrix.example.com"}'; return 200 '{"m.server": "matrix.example.com"}';
default_type application/json; default_type application/json;
@ -44,18 +44,18 @@ server {
default_type application/json; default_type application/json;
} }
location /.well-known/element/element.json { location /.well-known/element/element.json {
return 200 '{"call": {"widget_url": "https://call.example.com"}}'; return 200 '{"call": {"widget_url": "https://call.example.com"}}';
default_type application/json; default_type application/json;
} }
location / { location / {
if ($scheme = http) { if ($scheme = http) {
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
} }
access_log /var/log/nginx/example-access.log; access_log /var/log/nginx/example-access.log;
error_log /var/log/nginx/example-error.log; error_log /var/log/nginx/example-error.log;
} }

View file

@ -3,27 +3,27 @@ server {
listen [::]:80; listen [::]:80;
listen 443 ssl http2; listen 443 ssl http2;
listen [::]:443 ssl http2; listen [::]:443 ssl http2;
ssl_certificate /etc/letsencrypt/live/element.example.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/element.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/element.example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/element.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/ssl/dhparams.pem; ssl_dhparam /etc/ssl/dhparams.pem;
server_name element.example.com; server_name element.example.com;
location / { location / {
if ($scheme = http) { if ($scheme = http) {
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
add_header X-Frame-Options SAMEORIGIN; add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff; add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "frame-ancestors 'self'"; add_header Content-Security-Policy "frame-ancestors 'self'";
} }
root /usr/share/element-web; root /usr/share/element-web;
index index.html; index index.html;
access_log /var/log/nginx/elementweb-access.log; access_log /var/log/nginx/elementweb-access.log;
error_log /var/log/nginx/elementweb-error.log; error_log /var/log/nginx/elementweb-error.log;
} }

View file

@ -1,14 +1,14 @@
server { server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/livekit.example.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/livekit.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/livekit.example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/livekit.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/ssl/dhparams.pem; ssl_dhparam /etc/ssl/dhparams.pem;
server_name livekit.example.com; server_name livekit.example.com;
# This is lk-jwt-service # This is lk-jwt-service
location ~ ^(/sfu/get|/healthz) { location ~ ^(/sfu/get|/healthz) {
proxy_pass http://[::1]:8080; proxy_pass http://[::1]:8080;
@ -18,20 +18,20 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location / { location / {
proxy_pass http://[::1]:7880; proxy_pass http://[::1]:7880;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
#add_header Access-Control-Allow-Origin "*" always; #add_header Access-Control-Allow-Origin "*" always;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
access_log /var/log/nginx/livekit-access.log; access_log /var/log/nginx/livekit-access.log;
error_log /var/log/nginx/livekit-error.log; error_log /var/log/nginx/livekit-error.log;
} }

View file

@ -17,7 +17,7 @@ server {
location ~ ^/_matrix/client/(r0|v3)/rooms/([^/]*)/report/(.*)$ { location ~ ^/_matrix/client/(r0|v3)/rooms/([^/]*)/report/(.*)$ {
# The r0 endpoint is deprecated but still used by many clients. # The r0 endpoint is deprecated but still used by many clients.
# As of this writing, the v3 endpoint is the up-to-date version. # As of this writing, the v3 endpoint is the up-to-date version.
# Alias the regexps, to ensure that they're not rewritten. # Alias the regexps, to ensure that they're not rewritten.
set $room_id $2; set $room_id $2;
set $event_id $3; set $event_id $3;
@ -53,7 +53,7 @@ server {
client_max_body_size 50M; client_max_body_size 50M;
proxy_http_version 1.1; proxy_http_version 1.1;
} }
# The rest of the admin endpoint shouldn't be public # The rest of the admin endpoint shouldn't be public
location ~ ^/_synapse/admin { location ~ ^/_synapse/admin {
allow 127.0.0.1; allow 127.0.0.1;

View file

@ -1,16 +1,16 @@
server { server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/ssl/dhparams.pem; ssl_dhparam /etc/ssl/dhparams.pem;
server_name admin.example.com; server_name admin.example.com;
root /var/www/synapse-admin; root /var/www/synapse-admin;
access_log /var/log/nginx/admin-access.log; access_log /var/log/nginx/admin-access.log;
error_log /var/log/nginx/admin-error.log; error_log /var/log/nginx/admin-error.log;
} }

View file

@ -282,7 +282,7 @@ Now that we have defined the workers and/or worker pools, we have to forward
the right traffic to the right workers. The Synapse documentation about the right traffic to the right workers. The Synapse documentation about
[available worker [available worker
types](https://element-hq.github.io/synapse/latest/workers.html#available-worker-applications) types](https://element-hq.github.io/synapse/latest/workers.html#available-worker-applications)
lists which endpoints a specific worker type can handle. lists which endpoints a specific worker type can handle.
## Login ## Login
@ -323,7 +323,7 @@ requests:
``` ```
We forward those to our 2 worker pools making sure the heavy initial syncs go We forward those to our 2 worker pools making sure the heavy initial syncs go
to the `initial_sync` pool, and the normal ones to `normal_sync`. We use the to the `initial_sync` pool, and the normal ones to `normal_sync`. We use the
variable `$sync`for that, which we defined in maps.conf. variable `$sync`for that, which we defined in maps.conf.
``` ```

View file

@ -2,7 +2,7 @@
# should be stored under /etc/nginx/conf.d so that it is loaded whenever nginx starts. # should be stored under /etc/nginx/conf.d so that it is loaded whenever nginx starts.
# List of allowed origins, can only send one. # List of allowed origins, can only send one.
map $http_origin $allow_origin { map $http_origin $allow_origin {
~^https?://element.example.com$ $http_origin; ~^https?://element.example.com$ $http_origin;
~^https?://call.example.com$ $http_origin; ~^https?://call.example.com$ $http_origin;
~^https?://someserver.example.com$ $http_origin; ~^https?://someserver.example.com$ $http_origin;

View file

@ -192,7 +192,7 @@ See the included files for more elaborate examples, and check
Synapse should probably be able to send out e-mails; notifications for those Synapse should probably be able to send out e-mails; notifications for those
who want that, and password reset for those who need one. who want that, and password reset for those who need one.
You configure this under the section `email` (yes, really). You configure this under the section `email` (yes, really).
First of all, you need an SMTP-server that is configured to send e-mail for First of all, you need an SMTP-server that is configured to send e-mail for
your domain. Configuring that is out of scope, we'll assume we can use the your domain. Configuring that is out of scope, we'll assume we can use the
@ -294,7 +294,7 @@ password_config:
With this bit, we configure Synapse to let users pick and change their own With this bit, we configure Synapse to let users pick and change their own
passwords, as long as they meet the configured conditions. Mind you: `pepper` is passwords, as long as they meet the configured conditions. Mind you: `pepper` is
a secret random string that should *NEVER* be changed after initial setup. a secret random string that should *NEVER* be changed after initial setup.
But in a bigger environment you'll probably want to use some authentication But in a bigger environment you'll probably want to use some authentication
backend, such as LDAP. LDAP is configured by means of a module (see backend, such as LDAP. LDAP is configured by means of a module (see

View file

@ -4,7 +4,7 @@ password_config:
policy: policy:
enabled: only_for_reauth enabled: only_for_reauth
localdb_enabled: false localdb_enabled: false
password_providers: password_providers:
- module: "ldap_auth_provider.LdapAuthProvider" - module: "ldap_auth_provider.LdapAuthProvider"
config: config:

View file

@ -153,7 +153,7 @@ listeners:
type: http type: http
resources: resources:
- names: - names:
- replication - replication
``` ```
This means Synapse will create two sockets under `/run/matrix-synapse`: one This means Synapse will create two sockets under `/run/matrix-synapse`: one

View file

@ -26,7 +26,7 @@ lib.mapAttrs (name: test: pkgs.testers.runNixOSTest (test // { inherit name; }))
# run all application-level tests managed by Django # run all application-level tests managed by Django
# https://docs.djangoproject.com/en/5.0/topics/testing/overview/ # https://docs.djangoproject.com/en/5.0/topics/testing/overview/
testScript = '' testScript = ''
server.succeed("manage test") server.succeed("manage test ${name}")
''; '';
}; };
admin = { admin = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -30,7 +30,7 @@
{% load custom_tags %} {% load custom_tags %}
<li> <li>
{% if user.is_authenticated %} {% if user.is_authenticated %}
Welcome, {{ user.username }}! <a id="logout" href="{% auth_url 'logout' %}">Logout</a> Welcome, <a href="{% url 'account_detail' %}">{{ user.username }}</a>! <a id="logout" href="{% auth_url 'logout' %}">Logout</a>
{% else %} {% else %}
<a id="login" href="{% auth_url 'login' %}">Login</a> <a id="login" href="{% auth_url 'login' %}">Login</a>
{% endif %} {% endif %}

View file

View file

@ -0,0 +1,104 @@
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from django.template import Template, Context
from urllib.parse import unquote
class Login(TestCase):
def setUp(self):
self.username = 'testuser'
self.password = 'securepassword123'
self.user = User.objects.create_user(
username=self.username,
email='test@example.com',
password=self.password
)
self.login = reverse('login')
self.logout = reverse('logout')
self.required_login = reverse('account_detail')
self.optional_login = reverse('service_list')
def test_optional_login_redirects_back_to_original_page(self):
# go to a view where authentication is optional
response = self.client.get(self.optional_login)
self.assertEqual(response.status_code, 200)
self.assertFalse(response.context['user'].is_authenticated)
# check that the expected login URL is in the response
context = response.context[0]
template = Template("{% load custom_tags %}{% auth_url 'login' %}")
login_url = template.render(context)
self.assertIn(login_url, response.content.decode('utf-8'))
# log in
response = self.client.get(login_url)
self.assertEqual(response.status_code, 200)
login_data = {
'username': self.username,
'password': self.password,
}
response = self.client.post(login_url, login_data, follow=True)
# check that we're back at the desired view and authenticated
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context['user'].is_authenticated)
location, status = response.redirect_chain[-1]
self.assertEqual(location, self.optional_login)
# check that the expected logout URL is present
context = response.context[0]
template = Template("{% load custom_tags %}{% auth_url 'logout' %}")
logout_url = template.render(context)
self.assertIn(logout_url, response.content.decode('utf-8'))
# log out again
response = self.client.get(logout_url, follow=True)
# check that we're back at the view and logged out
self.assertEqual(response.status_code, 200)
location, status = response.redirect_chain[-1]
self.assertEqual(location, self.optional_login)
self.assertFalse(response.context['user'].is_authenticated)
def test_required_login_redirects_back_login(self):
# go to a view that requires authentication
response = self.client.get(self.required_login)
# check that we're redirected to the login view
self.assertEqual(response.status_code, 302)
redirect = response.url
self.assertTrue(redirect.startswith(self.login))
# log in
response = self.client.get(redirect)
self.assertEqual(response.status_code, 200)
login_data = {
'username': self.username,
'password': self.password,
}
response = self.client.post(redirect, login_data, follow=True)
# check that we reached the desired view, authenticated
self.assertEqual(response.status_code, 200)
location, status = response.redirect_chain[-1]
self.assertEqual(location, self.required_login)
self.assertTrue(response.context['user'].is_authenticated)
# check that the expected logout URL is present
context = response.context[0]
template = Template("{% load custom_tags %}{% auth_url 'logout' %}")
logout_url = template.render(context)
self.assertIn(logout_url, response.content.decode('utf-8'))
# log out
response = self.client.get(logout_url, follow=True)
# check that we're at the expected location, logged out
self.assertEqual(response.status_code, 200)
template = Template("{% load custom_tags %}{% auth_url 'login' %}")
login_url = template.render(context)
location, status = response.redirect_chain[-1]
self.assertEqual(location, unquote(login_url))
self.assertFalse(response.context['user'].is_authenticated)

View file

@ -3,54 +3,54 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.column { .column {
flex-basis: calc(50% - 10px); /* Adjust width as necessary */ flex-basis: calc(50% - 10px); /* Adjust width as necessary */
} }
.list { .list {
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;
} }
.list-item { .list-item {
margin-bottom: 10px; margin-bottom: 10px;
} }
.link { .link {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
.title { .title {
font-weight: bold; font-weight: bold;
} }
.hr-list { .hr-list {
border: 0; border: 0;
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;
} }
.list-item { .list-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.content { .content {
flex: 1; flex: 1;
} }
.link { .link {
text-align: left; text-align: left;
} }
.time { .time {
text-align: right; text-align: right;
} }
.grid-container { .grid-container {
display: grid; display: grid;
@ -78,12 +78,12 @@
.read-more-link { .read-more-link {
color: #FF6E00; /* Use the variable defined in theme.json */ color: #FF6E00; /* Use the variable defined in theme.json */
} }
.center-wrapper { .center-wrapper {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.grid-container-small { .grid-container-small {
@ -102,7 +102,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.hr-list2 { .hr-list2 {
@ -115,7 +115,7 @@
.header-with-image2 { .header-with-image2 {
text-align: center; text-align: center;
} }
.header-with-image2 img { .header-with-image2 img {
display: inline-block; display: inline-block;

View file

@ -9,124 +9,160 @@ in
collections.news.type = cfg.content-types.article; collections.news.type = cfg.content-types.article;
collections.events.type = cfg.content-types.event; collections.events.type = cfg.content-types.event;
pages.index = { config, link, ... }: { pages.index =
title = "Welcome to the Fediversity project"; { config, link, ... }:
description = "Fediversity web site"; {
summary = '' title = "Welcome to the Fediversity project";
This web site hosts up-to-date information about the the NGI Zero Fediversity project. description = "Fediversity web site";
''; summary = ''
body = '' This web site hosts up-to-date information about the the NGI Zero Fediversity project.
${pages.fediversity.summary} '';
body = ''
${pages.fediversity.summary}
[Learn more about Fediversity](${link pages.fediversity}) [Learn more about Fediversity](${link pages.fediversity})
''; '';
outputs.html = (cfg.templates.html.page config).override (final: prev: { outputs.html = (cfg.templates.html.page config).override (
html = { _final: prev: {
head.title.text = "Fediversity"; html = {
head.link.stylesheets = prev.html.head.link.stylesheets ++ [ head.title.text = "Fediversity";
{ href = "${link cfg.assets."index.css"}"; } head.link.stylesheets = prev.html.head.link.stylesheets ++ [
]; { href = "${link cfg.assets."index.css"}"; }
body.content = ];
let body.content =
to-section = { heading, body, attrs ? { } }: { let
section = { to-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"; heading,
body = '' body,
${pages.grants.summary} attrs ? { },
}:
[Learn more about Fediversity grants](${link pages.grants})
'';
}
{ {
heading = "Consortium"; section = {
body = '' heading.content = heading;
The Consortium behind the Fediversity project is a cooperation between NLnet, Open Internet Discourse Foundation, NORDUnet and Tweag. 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}
${toString (map (partner: '' [Learn more about Fediversity grants](${link pages.grants})
### ${partner.title} '';
}
{
heading = "Consortium";
body = ''
The Consortium behind the Fediversity project is a cooperation between NLnet, Open Internet Discourse Foundation, NORDUnet and Tweag.
${partner.summary} ${toString (
map
(partner: ''
### ${partner.title}
[Read more about ${partner.title}](${link partner}) ${partner.summary}
'') (with pages; [ nlnet oid tweag nordunet ]))}
'';
}
{
heading = "Fediverse explained";
body = ''
${toString (map (role: ''
### ${role.title}
${role.summary} [Read more about ${partner.title}](${link partner})
'')
(
with pages;
[
nlnet
oid
tweag
nordunet
]
)
)}
'';
}
{
heading = "Fediverse explained";
body = ''
${toString (
map
(role: ''
### ${role.title}
[Read more about ${role.title}](${link role}) ${role.summary}
'') (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);
}
]);
};
}); [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" );
'' };
assets."index.css".path =
with lib;
builtins.toFile "index.css" ''
section h1, section h2, section h3 section h1, section h2, section h3
{ {
text-align: center; text-align: center;

View file

@ -1,22 +1,26 @@
{ config, lib, ... }: { config, lib, ... }:
{ {
pages.events = { link, ... }: rec { pages.events =
title = "Events"; { link, ... }:
description = "Events related to the Fediverse and NixOS"; rec {
summary = description; title = "Events";
body = description = "Events related to the Fediverse and NixOS";
with lib; summary = description;
let body =
events = map with lib;
(event: with lib; '' let
## [${event.title}](${link event}) 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} ${event.start-date} ${
'') optionalString (!isNull event.end-date && event.end-date != event.start-date) "to ${event.end-date}"
config.collections.events.entry; } in ${event.location}
in ''
'' ) config.collections.events.entry;
${join "\n" events} in
''; ''
}; ${join "\n" events}
'';
};
} }

View file

@ -1,23 +1,25 @@
{ config, lib, ... }: { config, ... }:
{ {
collections.events.entry = { link, ... }: { collections.events.entry =
title = "NixOS 24.11 ZHF hackathon"; { link, ... }:
name = "zhf-24-11"; {
description = "NixOS 24.11 ZHF hackathon in Zürich"; title = "NixOS 24.11 ZHF hackathon";
start-date = "2024-11-23"; name = "zhf-24-11";
end-date = "2024-11-24"; description = "NixOS 24.11 ZHF hackathon in Zürich";
start-time = "10:00"; start-date = "2024-11-23";
end-time = "17:00"; end-date = "2024-11-24";
location = "OST Campus Rapperswil"; start-time = "10:00";
body = '' end-time = "17:00";
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. 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*. 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. 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. 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. [Fediversity engineers attended](${link config.collections.news.by-name.zhf-24-11}) to present prototypes and exchange ideas with other developers.
''; '';
}; };
} }

View file

@ -1,24 +1,26 @@
{ ... }: { ... }:
{ {
collections.events.entry = { ... }: { collections.events.entry =
title = "OW2con 2024"; { ... }:
description = "OW2con is the annual European open source conference in Paris"; {
start-date = "2024-06-11"; title = "OW2con 2024";
end-date = "2024-06-12"; description = "OW2con is the annual European open source conference in Paris";
start-time = "09:00"; start-date = "2024-06-11";
end-time = "18:00"; end-date = "2024-06-12";
location = "Paris-Chatillon"; start-time = "09:00";
body = '' end-time = "18:00";
OW2con is the European open source conference organized by OW2. location = "Paris-Chatillon";
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. body = ''
It also offers a unique opportunity to establish contact with peers through friendly networking sessions. OW2con is the European open source conference organized by OW2.
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. 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 OW2con24 call for presentations is open. The OW2con24 call for presentations is open.
This year we are giving the highlight on the theme of open source funding: 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? What are the current solutions for innovators, start-ups or ISVs to finance their development?
Private or public financing? Private or public financing?
Are national and European public policies up to the challenges? Are national and European public policies up to the challenges?
''; '';
}; };
} }

View file

@ -1,18 +1,20 @@
{ ... }: { ... }:
{ {
collections.events.entry = { ... }: { collections.events.entry =
title = "PublicSpaces Conference 2024"; { ... }:
description = "A conference by PublicSpaces, Taking Back the Internet."; {
start-date = "2024-06-06"; title = "PublicSpaces Conference 2024";
end-date = "2024-06-07"; description = "A conference by PublicSpaces, Taking Back the Internet.";
start-time = "09:00"; start-date = "2024-06-06";
end-time = "18:00"; end-date = "2024-06-07";
location = "Pakhuis de Zwijger - Amsterdam"; start-time = "09:00";
body = '' end-time = "18:00";
On June 6th and 7th, PublicSpaces and Waag Futurelab proudly present the fourth edition of the PublicSpaces conference under the theme 'Empowering the Internet'. location = "Pakhuis de Zwijger - Amsterdam";
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. body = ''
Join us as we navigate towards a digital landscape where everyone has a voice. On June 6th and 7th, PublicSpaces and Waag Futurelab proudly present the fourth edition of the PublicSpaces conference under the theme 'Empowering the Internet'.
For more information, check out the [website](https://publicspaces.net/2024/02/01/save-the-date-publicspaces-conferentie-2024/) 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/)
'';
};
} }

View file

@ -1,25 +1,27 @@
{ ... }: { ... }:
{ {
collections.events.entry = { ... }: { collections.events.entry =
title = "State of the Internet 2024"; { ... }:
description = "The State of the Internet 2024 by Waag"; {
start-date = "2024-05-16"; title = "State of the Internet 2024";
end-date = "2024-05-16"; description = "The State of the Internet 2024 by Waag";
start-time = "18:00"; start-date = "2024-05-16";
end-time = "20:00"; end-date = "2024-05-16";
location = "OBA Oosterdok - Amsterdam"; start-time = "18:00";
body = '' end-time = "20:00";
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. location = "OBA Oosterdok - Amsterdam";
Featuring Kim van Sparrentak, Member of the European Parliament, discussing Europe's efforts to regulate Big Tech and enhance digital rights. body = ''
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. 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: The event takes place at:
OBA Oosterdok <br> OBA Oosterdok <br>
Oosterdokskade 143 <br> Oosterdokskade 143 <br>
1011 DK Amsterdam 1011 DK Amsterdam
Registration available [here](https://waag.org/nl/event/de-staat-van-het-internet-2024-met-kim-van-sparrentak/). Registration available [here](https://waag.org/nl/event/de-staat-van-het-internet-2024-met-kim-van-sparrentak/).
''; '';
}; };
} }

View file

@ -1,4 +1,4 @@
{ config, lib, ... }: { config, ... }:
let let
inherit (config) pages; inherit (config) pages;
in in
@ -6,16 +6,33 @@ in
menus.main = { menus.main = {
label = "Main"; label = "Main";
items = [ items = [
{ page = pages.index // { title = "Start"; }; } {
page = pages.index // {
title = "Start";
};
}
{ {
menu.label = "For you"; menu.label = "For you";
menu.items = map (page: { inherit page; }) menu.items = map (page: { inherit page; }) (
(with pages; [ individuals developers european-commission ]); with pages;
[
individuals
developers
european-commission
]
);
} }
{ {
menu.label = "Consortium"; menu.label = "Consortium";
menu.items = map (page: { inherit page; }) menu.items = map (page: { inherit page; }) (
(with pages; [ nlnet oid tweag nordunet ]); with pages;
[
nlnet
oid
tweag
nordunet
]
);
} }
{ page = pages.fediversity; } { page = pages.fediversity; }
{ page = pages.grants; } { page = pages.grants; }

View file

@ -1,24 +1,24 @@
{ config, lib, ... }: { config, lib, ... }:
{ {
pages.news = { link, ... }: rec { pages.news =
title = "News"; { link, ... }:
description = "News about Fediversity"; rec {
summary = description; title = "News";
body = description = "News about Fediversity";
with lib; summary = description;
let body =
news = map with lib;
(article: '' let
news = map (article: ''
## [${article.title}](${link article}) ## [${article.title}](${link article})
${article.date} by ${article.author} ${article.date} by ${article.author}
${article.summary} ${article.summary}
'') '') config.collections.news.entry;
config.collections.news.entry; in
in ''
'' ${join "\n\n" news}
${join "\n\n" news} '';
''; };
};
} }

View file

@ -1,22 +1,24 @@
{ config, lib, ... }: { config, ... }:
{ {
collections.news.entry = { link, ... }: rec { collections.news.entry =
name = "zhf-24-11"; { link, ... }:
title = "NixOS 24.11 release hackathon and workshop"; rec {
description = "Fediversity engineers met in Zürich at a NixOS 24.11 ZHF hackathon"; name = "zhf-24-11";
date = "2024-11-28"; title = "NixOS 24.11 release hackathon and workshop";
author = "Valentin Gagarin"; description = "Fediversity engineers met in Zürich at a NixOS 24.11 ZHF hackathon";
summary = '' date = "2024-11-28";
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. author = "Valentin Gagarin";
''; summary = ''
body = '' 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.
${summary} '';
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. 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. 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. 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! This was an extraordinarily fruitful encounter that helped us to identify overlaps and potential for future collaboration!
''; '';
}; };
} }

View file

@ -1,19 +1,22 @@
{ config, lib, ... }: { ... }:
{ {
collections.news.entry = { link, ... }: { collections.news.entry =
title = "Fediversity project publicly announced"; { ... }:
description = "The Fediversity project has officially been announced"; {
date = "2024-01-01"; title = "Fediversity project publicly announced";
author = "Laurens Hof"; description = "The Fediversity project has officially been announced";
summary = '' date = "2024-01-01";
We are pleased to introduce the launch of our new website dedicated to the Fediversity project. author = "Laurens Hof";
''; summary = ''
body = '' We are pleased to introduce the launch of our new website dedicated to the Fediversity project.
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. '';
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 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 Unions Horizon Europe research and innovation programme under grant agreement No. 101136078. Fediversity has received funding from the European Unions Horizon Europe research and innovation programme under grant agreement No. 101136078.
''; '';
}; };
} }

View file

@ -34,7 +34,7 @@ features3:
button: button:
enable: true enable: true
label: "Learn more" label: "Learn more"
link: "/oid" link: "/oid"
- title: "Tweag" - title: "Tweag"
image: "/images/users.svg" image: "/images/users.svg"
@ -42,7 +42,7 @@ features3:
button: button:
enable: true enable: true
label: "Learn more" label: "Learn more"
link: "/tweag" link: "/tweag"
- title: "NORDUnet" - title: "NORDUnet"
image: "/images/users.svg" image: "/images/users.svg"
@ -50,7 +50,7 @@ features3:
button: button:
enable: true enable: true
label: "Learn more" label: "Learn more"
link: "/nordunet" link: "/nordunet"
features: features:
- title: "Fediversity Grants" - title: "Fediversity Grants"
@ -59,7 +59,7 @@ features:
button: button:
enable: true enable: true
label: "Learn more" label: "Learn more"
link: "/grants" link: "/grants"
features2: features2:
- title: "Individuals" - title: "Individuals"

View file

@ -1,14 +1,16 @@
{ sources ? import ../npins {
, system ? builtins.currentSystem sources ? import ../npins,
, pkgs ? import sources.nixpkgs { system ? builtins.currentSystem,
pkgs ? import sources.nixpkgs {
inherit system; inherit system;
config = { }; config = { };
overlays = [ ]; overlays = [ ];
} },
, lib ? import "${sources.nixpkgs}/lib" lib ? import "${sources.nixpkgs}/lib",
}: }:
let let
lib' = final: prev: lib' =
final: prev:
let let
new = import ./lib.nix { lib = final; }; new = import ./lib.nix { lib = final; };
in in
@ -37,15 +39,21 @@ rec {
let let
run-tests = pkgs.writeShellApplication { run-tests = pkgs.writeShellApplication {
name = "run-tests"; name = "run-tests";
text = with pkgs; with lib; '' text =
${getExe nix-unit} ${toString ./tests.nix} "$@" with pkgs;
''; with lib;
''
${getExe nix-unit} ${toString ./tests.nix} "$@"
'';
}; };
test-loop = pkgs.writeShellApplication { test-loop = pkgs.writeShellApplication {
name = "test-loop"; name = "test-loop";
text = with pkgs; with lib; '' text =
${getExe watchexec} -w ${toString ./.} -- ${getExe nix-unit} ${toString ./tests.nix} with pkgs;
''; with lib;
''
${getExe watchexec} -w ${toString ./.} -- ${getExe nix-unit} ${toString ./tests.nix}
'';
}; };
devmode = pkgs.devmode.override { devmode = pkgs.devmode.override {
buildArgs = "${toString ./.} -A build --show-trace"; buildArgs = "${toString ./.} -A build --show-trace";
@ -62,7 +70,9 @@ rec {
}; };
inherit sources pkgs; inherit sources pkgs;
tests = with pkgs; with lib; tests =
with pkgs;
with lib;
let let
source = fileset.toSource { source = fileset.toSource {
root = ../.; root = ../.;

View file

@ -18,5 +18,5 @@
</div> </div>
</div> </div>
</section> </section>
{{ end }} {{ end }}

View file

@ -58,7 +58,7 @@
</div> </div>
</article> </article>
</div> </div>

View file

@ -61,4 +61,4 @@
</div> </div>
</section> </section>
{{ end }} {{ end }}

View file

@ -14,14 +14,14 @@
<a class="link" href="{{ .RelPermalink }}" style="color: #FF6E00">{{ .Title }}</a> <a class="link" href="{{ .RelPermalink }}" style="color: #FF6E00">{{ .Title }}</a>
</div> </div>
<hr class="hr-list"> <hr class="hr-list">
<time class="g time" datetime="{{ dateFormat "2006-01-02" .Date }}">{{ dateFormat "02-01-2006" .Date }}</time> <time class="g time" datetime="{{ dateFormat "2006-01-02" .Date }}">{{ dateFormat "02-01-2006" .Date }}</time>
<hr class="hr-list2"> <hr class="hr-list2">
</li> </li>
<div class="line"></div> <div class="line"></div>
{{ end }} {{ end }}
</ul> </ul>
</section> </section>
{{ end }} {{ end }}
</ul> </ul>

View file

@ -63,7 +63,7 @@
</div> </div>
</article> </article>
</div> </div>

View file

@ -45,7 +45,7 @@
<div class="container"> <div class="container">
<div class="grid-container"> <div class="grid-container">
{{ range $i, $e := .Params.features3 }} {{ range $i, $e := .Params.features3 }}
<div class="grid-item"> <div class="grid-item">
<div class="header-with-image2"> <div class="header-with-image2">
@ -89,7 +89,7 @@
> >
<h2 class="mb-4">{{ .title | markdownify }}</h2> <h2 class="mb-4">{{ .title | markdownify }}</h2>
<p class="mb-8 text-lg">{{ .content | markdownify }}</p> <p class="mb-8 text-lg">{{ .content | markdownify }}</p>
<ul> <ul>
{{ range .bulletpoints }} {{ range .bulletpoints }}
<li class="relative mb-4 pl-6"> <li class="relative mb-4 pl-6">
<i class="fa fa-check absolute left-0 top-1.5"></i> <i class="fa fa-check absolute left-0 top-1.5"></i>
@ -147,7 +147,7 @@
{{ if gt (len (where .Site.RegularPages "Section" "blog")) 0 }} {{ if gt (len (where .Site.RegularPages "Section" "blog")) 0 }}
<section> <section>
<div class="center-wrapper"> <div class="center-wrapper">
<div class="grid-container-small"> <div class="grid-container-small">
<div class="column"> <div class="column">
<ul class="list"> <ul class="list">
@ -164,7 +164,7 @@
<a class="link" href="{{ .RelPermalink }}" style="color: #FF6E00">{{ .Title }}</a> <a class="link" href="{{ .RelPermalink }}" style="color: #FF6E00">{{ .Title }}</a>
</div> </div>
<hr class="hr-list"> <hr class="hr-list">
<time class="g time" datetime="{{ dateFormat "2006-01-02" .Date }}">{{ dateFormat "02-01-2006" .Date }}</time> <time class="g time" datetime="{{ dateFormat "2006-01-02" .Date }}">{{ dateFormat "02-01-2006" .Date }}</time>
<hr class="hr-list2"> <hr class="hr-list2">
</li> </li>
@ -189,7 +189,7 @@
<a class="link" href="{{ .RelPermalink }}" style="color: #FF6E00">{{ .Title }}</a> <a class="link" href="{{ .RelPermalink }}" style="color: #FF6E00">{{ .Title }}</a>
</div> </div>
<hr class="hr-list"> <hr class="hr-list">
<time class="g time" datetime="{{ dateFormat "2006-01-02" .Date }}">{{ dateFormat "02-01-2006" .Date }}</time> <time class="g time" datetime="{{ dateFormat "2006-01-02" .Date }}">{{ dateFormat "02-01-2006" .Date }}</time>
<hr class="hr-list2"> <hr class="hr-list2">
</li> </li>

View file

@ -1,22 +1,26 @@
{ lib }: { lib }:
rec { rec {
template = g: f: x: template =
g: f: x:
let let
base = f x; base = f x;
result = g base; result = g base;
in in
result // { result
override = new: // {
override =
new:
let let
base' = base' =
if lib.isFunction new if lib.isFunction new then
then lib.recursiveUpdate base (new base' base) lib.recursiveUpdate base (new base' base)
else else
lib.recursiveUpdate base new; lib.recursiveUpdate base new;
result' = g base'; result' = g base';
in in
result' // { result'
override = new: (template g (x': base') x).override new; // {
override = new: (template g (_: base') x).override new;
}; };
}; };
@ -28,7 +32,8 @@ rec {
replaceStringRec "--" "-" "hello-----world" replaceStringRec "--" "-" "hello-----world"
=> "hello-world" => "hello-world"
*/ */
replaceStringsRec = from: to: string: replaceStringsRec =
from: to: string:
let let
replaced = lib.replaceStrings [ from ] [ to ] string; replaced = lib.replaceStrings [ from ] [ to ] string;
in in
@ -37,25 +42,24 @@ rec {
/** /**
Create a URL-safe slug from any string Create a URL-safe slug from any string
*/ */
slug = str: slug =
str:
let let
# Replace non-alphanumeric characters with hyphens # Replace non-alphanumeric characters with hyphens
replaced = join "" replaced = join "" (
( builtins.map (c: if (c >= "a" && c <= "z") || (c >= "0" && c <= "9") then c else "-") (
builtins.map with lib; stringToCharacters (toLower str)
(c: )
if (c >= "a" && c <= "z") || (c >= "0" && c <= "9") );
then c
else "-"
)
(with lib; stringToCharacters (toLower str)));
# Remove leading and trailing hyphens # Remove leading and trailing hyphens
trimHyphens = s: trimHyphens =
s:
let let
matched = builtins.match "(-*)([^-].*[^-]|[^-])(-*)" s; matched = builtins.match "(-*)([^-].*[^-]|[^-])(-*)" s;
in in
with lib; optionalString (!isNull matched) (builtins.elemAt matched 1); with lib;
optionalString (!isNull matched) (builtins.elemAt matched 1);
in in
trimHyphens (replaceStringsRec "--" "-" replaced); trimHyphens (replaceStringsRec "--" "-" replaced);
@ -64,9 +68,11 @@ rec {
/** /**
Trim trailing spaces and squash non-leading spaces Trim trailing spaces and squash non-leading spaces
*/ */
trim = string: trim =
string:
let let
trimLine = line: trimLine =
line:
with lib; with lib;
let let
# separate leading spaces from the rest # separate leading spaces from the rest
@ -76,8 +82,7 @@ rec {
# drop trailing spaces # drop trailing spaces
body = head (split " *$" rest); body = head (split " *$" rest);
in in
if body == "" then "" else if body == "" then "" else spaces + replaceStringsRec " " " " body;
spaces + replaceStringsRec " " " " body;
in in
join "\n" (map trimLine (splitLines string)); join "\n" (map trimLine (splitLines string));
@ -85,84 +90,95 @@ rec {
splitLines = s: with builtins; filter (x: !isList x) (split "\n" s); splitLines = s: with builtins; filter (x: !isList x) (split "\n" s);
indent = prefix: s: indent =
prefix: s:
with lib.lists; with lib.lists;
let let
lines = splitLines s; lines = splitLines s;
in in
join "\n" ( join "\n" ([ (head lines) ] ++ (map (x: if x == "" then x else "${prefix}${x}") (tail lines)));
[ (head lines) ]
++
(map (x: if x == "" then x else "${prefix}${x}") (tail lines))
);
relativePath = path1': path2': relativePath =
path1': path2':
let let
inherit (lib.path) subpath; inherit (lib.path) subpath;
inherit (lib) lists length take drop min max; inherit (lib)
lists
length
take
drop
min
max
;
path1 = subpath.components path1'; path1 = subpath.components path1';
prefix1 = take (length path1 - 1) path1; prefix1 = take (length path1 - 1) path1;
path2 = subpath.components path2'; path2 = subpath.components path2';
prefix2 = take (length path2 - 1) path2; prefix2 = take (length path2 - 1) path2;
commonPrefixLength = with lists; commonPrefixLength =
findFirstIndex (i: i.fst != i.snd) with lists;
(min (length prefix1) (length prefix2)) findFirstIndex (i: i.fst != i.snd) (min (length prefix1) (length prefix2)) (
(zipLists prefix1 prefix2); zipLists prefix1 prefix2
);
depth = max 0 (length prefix1 - commonPrefixLength); depth = max 0 (length prefix1 - commonPrefixLength);
relativeComponents = with lists; relativeComponents =
with lists;
[ "." ] ++ (replicate depth "..") ++ (drop commonPrefixLength path2); [ "." ] ++ (replicate depth "..") ++ (drop commonPrefixLength path2);
in in
join "/" relativeComponents; join "/" relativeComponents;
/** /**
Recursively list all Nix files from a directory, except the top-level `default.nix` Recursively list all Nix files from a directory, except the top-level `default.nix`
Useful for module system `imports` from a top-level module. Useful for module system `imports` from a top-level module.
**/ *
nixFiles = dir: with lib.fileset; */
toList (difference nixFiles =
(fileFilter ({ hasExt, ... }: hasExt "nix") dir) dir:
(dir + "/default.nix") with lib.fileset;
); toList (difference (fileFilter ({ hasExt, ... }: hasExt "nix") dir) (dir + "/default.nix"));
types = rec { types = rec {
# arbitrarily nested attribute set where the leaves are of type `type` # arbitrarily nested attribute set where the leaves are of type `type`
# NOTE: this works for anything but attribute sets! # NOTE: this works for anything but attribute sets!
recursiveAttrs = type: with lib.types; recursiveAttrs =
type:
with lib.types;
# NOTE: due to how `either` works, the first match is significant, # NOTE: due to how `either` works, the first match is significant,
# so if `type` happens to be an attrset, the typecheck will consider # so if `type` happens to be an attrset, the typecheck will consider
# `type`, not `attrsOf` # `type`, not `attrsOf`
attrsOf (either type (recursiveAttrs type)); 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 of unnamed items that can be added to item-wise, i.e. without wrapping the item in a list
collection = elemType: collection =
elemType:
let let
unparenthesize = class: class == "noun"; unparenthesize = class: class == "noun";
desc = type: desc = type: types.optionDescriptionPhrase unparenthesize type;
types.optionDescriptionPhrase unparenthesize type; desc' =
desc' = type: type:
let let
typeDesc = lib.types.optionDescriptionPhrase unparenthesize type; typeDesc = lib.types.optionDescriptionPhrase unparenthesize type;
in in
if type.descriptionClass == "noun" if type.descriptionClass == "noun" then typeDesc + "s" else "many instances of ${typeDesc}";
then
typeDesc + "s"
else
"many instances of ${typeDesc}";
in in
lib.types.mkOptionType { lib.types.mkOptionType {
name = "collection"; name = "collection";
description = "separately specified ${desc elemType} for a collection of ${desc' elemType}"; description = "separately specified ${desc elemType} for a collection of ${desc' elemType}";
merge = loc: defs: merge =
map loc: defs:
(def: map (
elemType.merge (loc ++ [ "[definition ${toString def.file}]" ]) [{ inherit (def) file; value = def.value; }] def:
) elemType.merge (loc ++ [ "[definition ${toString def.file}]" ]) [
defs; {
inherit (def) file;
value = def.value;
}
]
) defs;
check = elemType.check; check = elemType.check;
getSubOptions = elemType.getSubOptions; getSubOptions = elemType.getSubOptions;
getSubModules = elemType.getSubModules; getSubModules = elemType.getSubModules;
@ -175,29 +191,34 @@ rec {
nestedTypes.elemType = elemType; nestedTypes.elemType = elemType;
}; };
listOfUnique = elemType: listOfUnique =
elemType:
let let
baseType = lib.types.listOf elemType; baseType = lib.types.listOf elemType;
in in
baseType // { baseType
merge = loc: defs: // {
merge =
loc: defs:
let let
# Keep track of which definition each value came from # Keep track of which definition each value came from
defsWithValues = map defsWithValues = map (
(def: def:
map (v: { inherit (def) file; value = v; }) def.value map (v: {
) inherit (def) file;
defs; value = v;
}) def.value
) defs;
flatDefs = lib.flatten defsWithValues; flatDefs = lib.flatten defsWithValues;
# Check for duplicates while preserving source info # Check for duplicates while preserving source info
seen = builtins.foldl' seen = builtins.foldl' (
(acc: def: acc: def:
if lib.lists.any (v: v.value == def.value) acc if lib.lists.any (v: v.value == def.value) acc then
then throw "The option `${lib.options.showOption loc}` has duplicate values (${toString def.value}) defined in ${def.file}" throw "The option `${lib.options.showOption loc}` has duplicate values (${toString def.value}) defined in ${def.file}"
else acc ++ [ def ] else
) [ ] acc ++ [ def ]
flatDefs; ) [ ] flatDefs;
in in
map (def: def.value) seen; map (def: def.value) seen;
}; };

View file

@ -1,4 +1,10 @@
{ config, options, lib, pkgs, ... }: {
config,
options,
lib,
pkgs,
...
}:
let let
inherit (lib) inherit (lib)
mkOption mkOption
@ -8,13 +14,12 @@ in
{ {
imports = lib.nixFiles ./.; imports = lib.nixFiles ./.;
options.templates = options.templates = mkOption {
mkOption { description = ''
description = '' Collection of named helper functions for conversion different structured representations which can be rendered to a string
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));
type = with types; recursiveAttrs (functionTo (either str attrs)); };
};
options.files = mkOption { options.files = mkOption {
description = '' description = ''
@ -32,58 +37,64 @@ in
type = types.package; type = types.package;
default = default =
let let
script = '' script =
mkdir $out ''
'' + lib.join "\n" copy; mkdir $out
copy = lib.mapAttrsToList ''
( + lib.join "\n" copy;
path: file: '' copy = lib.mapAttrsToList (path: file: ''
mkdir -p $out/$(dirname ${path}) mkdir -p $out/$(dirname ${path})
cp -r ${file} $out/${path} cp -r ${file} $out/${path}
'' '') config.files;
)
config.files;
in in
pkgs.runCommand "source" { } script; pkgs.runCommand "source" { } script;
}; };
# TODO: this is an artefact of exploration; needs to be adapted to actual use # TODO: this is an artefact of exploration; needs to be adapted to actual use
config.templates.table-of-contents = { config, ... }: config.templates.table-of-contents =
{ config, ... }:
let let
outline = { ... }: { outline =
options = { { ... }:
value = mkOption { {
# null denotes root options = {
type = with types; nullOr (either str (listOf (attrTag categories.phrasing))); value = mkOption {
subsections = mkOption { # null denotes root
type = with types; listOf (submodule outline); type = with types; nullOr (either str (listOf (attrTag categories.phrasing)));
default = with lib; map subsections = mkOption {
# TODO: go into depth manually here, type = with types; listOf (submodule outline);
# we don't want to pollute the DOM implementation default =
(c: (lib.head (attrValues c)).outline) with lib;
(filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content); 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 ""}
'';
}; };
}; };
__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 in
{ {
options.outline = mkOption { options.outline = mkOption {
type = types.submodule outline; type = types.submodule outline;
default = { default = {
value = null; value = null;
subsections = with lib; subsections =
map (c: (lib.head (attrValues c)).outline) with lib;
(filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content); map (c: (lib.head (attrValues c)).outline) (
filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content
);
}; };
}; };
}; };

File diff suppressed because it is too large Load diff

View file

@ -1,27 +1,53 @@
{ config, lib, pkgs, ... }: { {
config,
lib,
pkgs,
...
}:
{
config.assets."style.css".path = ./style.css; config.assets."style.css".path = ./style.css;
config.assets."ngi-fediversity.svg".path = ./ngi-fediversity.svg; config.assets."ngi-fediversity.svg".path = ./ngi-fediversity.svg;
# TODO: auto-generate a bunch from SVG # TODO: auto-generate a bunch from SVG
config.assets."favicon.png".path = ./favicon.png; config.assets."favicon.png".path = ./favicon.png;
config.assets."fonts.css".path = with lib; builtins.toFile "fonts.css" (join "\n" (map config.assets."fonts.css".path =
(font: '' with lib;
@font-face { builtins.toFile "fonts.css" (
font-family: '${font.name}'; join "\n" (
font-style: normal; map
font-weight: ${toString font.weight}; (font: ''
src: url(/${head config.assets.${font.file}.locations}) format('woff2'); @font-face {
} font-family: '${font.name}';
'') font-style: normal;
( font-weight: ${toString font.weight};
(crossLists (name: file: weight: { inherit name file weight; }) src: url(/${head config.assets.${font.file}.locations}) format('woff2');
[ [ "Signika" ] [ "signika-extended.woff2" "signika.woff2" ] [ 500 700 ] ] }
'')
(
(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
]
])
)
) )
++ );
(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 # TODO: get directly from https://github.com/google/fonts
# and compress with https://github.com/fonttools/fonttools # and compress with https://github.com/fonttools/fonttools

View file

@ -1,17 +1,21 @@
{ config, options, lib, pkgs, ... }: {
let config,
inherit (lib) lib,
mkOption pkgs,
types ...
; }:
in
{ {
config.templates.html = { config.templates.html = {
dom = document: dom =
document:
let let
eval = lib.evalModules { eval = lib.evalModules {
class = "DOM"; class = "DOM";
modules = [ document (import ./dom.nix) ]; modules = [
document
(import ./dom.nix)
];
}; };
in in
{ {
@ -19,27 +23,34 @@ in
value = eval.config; value = eval.config;
}; };
markdown = { name, body }: markdown =
{ name, body }:
let let
commonmark = pkgs.runCommand "${name}.html" commonmark =
{ pkgs.runCommand "${name}.html"
buildInputs = [ pkgs.cmark ]; {
} '' buildInputs = [ pkgs.cmark ];
cmark ${builtins.toFile "${name}.md" body} > $out }
''; ''
cmark ${builtins.toFile "${name}.md" body} > $out
'';
in in
builtins.readFile commonmark; builtins.readFile commonmark;
nav = { menu, page }: nav =
{ menu, page }:
let let
render-item = item: render-item =
if item ? menu then '' item:
<li><details><summary>${item.menu.label}</summary> if item ? menu then
${lib.indent " " (item.menu.outputs.html page)} ''
</li> <li><details><summary>${item.menu.label}</summary>
'' ${lib.indent " " (item.menu.outputs.html page)}
else if item ? page then ''<li><a href="${page.link item.page}">${item.page.title}</a></li>'' </li>
else ''<li><a href="${item.link.url}">${item.link.label}</a></li>'' ''
; else if item ? page then
''<li><a href="${page.link item.page}">${item.page.title}</a></li>''
else
''<li><a href="${item.link.url}">${item.link.label}</a></li>'';
in in
'' ''
<nav> <nav>
@ -50,17 +61,27 @@ in
''; '';
}; };
config.templates.files = fs: with lib; config.templates.files =
fs:
with lib;
foldl' foldl'
# TODO: create static redirects from `tail <collection>.locations` # TODO: create static redirects from `tail <collection>.locations`
(acc: elem: acc // (mapAttrs' (type: value: { (
name = head elem.locations + optionalString (type != "") ".${type}"; acc: elem:
value = if isStorePath value then value else acc
builtins.toFile //
(elem.name + optionalString (type != "") ".${type}") (mapAttrs' (
(toString value); type: value: {
})) name = head elem.locations + optionalString (type != "") ".${type}";
elem.outputs) value =
if isStorePath value then
value
else
builtins.toFile (elem.name + optionalString (type != "") ".${type}") (toString value);
}
))
elem.outputs
)
{ } { }
fs; fs;
} }

View file

@ -1,51 +1,62 @@
{ config, options, lib, ... }: {
config,
options,
lib,
...
}:
let let
inherit (lib) mkOption inherit (lib)
mkOption
types types
; ;
cfg = config; cfg = config;
in in
{ {
content-types.article = { config, collection, ... }: { content-types.article =
imports = [ cfg.content-types.page ]; { config, collection, ... }:
options = { {
collection = mkOption { imports = [ cfg.content-types.page ];
description = "Collection this article belongs to"; options = {
type = options.collections.type.nestedTypes.elemType; collection = mkOption {
default = collection; description = "Collection this article belongs to";
}; type = options.collections.type.nestedTypes.elemType;
date = mkOption { default = collection;
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;
}; };
})); 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;
};
}
)
);
};
} }

View file

@ -11,24 +11,34 @@ in
description = '' description = ''
Collection of assets, i.e. static files that can be linked to from within documents Collection of assets, i.e. static files that can be linked to from within documents
''; '';
type = with types; attrsOf (submodule ({ config, ... }: { type =
imports = [ cfg.content-types.document ]; with types;
options.path = mkOption { attrsOf (
type = types.path; submodule (
}; { config, ... }:
config.outputs."" = if lib.isStorePath config.path then config.path else "${config.path}"; {
})); 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 = { }; default = { };
}; };
config.files = with lib; config.files =
with lib;
let let
flatten = attrs: mapAttrsToList flatten =
(name: value: attrs:
mapAttrsToList (
_name: value:
# HACK: we somehow have to distinguish a module value from regular attributes. # HACK: we somehow have to distinguish a module value from regular attributes.
# arbitrary choice: the outputs attribute # arbitrary choice: the outputs attribute
if value ? outputs then value else mapAttrsToList value) if value ? outputs then value else mapAttrsToList value
attrs; ) attrs;
in in
cfg.templates.files (flatten cfg.assets); cfg.templates.files (flatten cfg.assets);
} }

View file

@ -1,4 +1,10 @@
{ config, options, lib, pkgs, ... }: {
config,
options,
lib,
...
}:
let let
inherit (lib) inherit (lib)
mkOption mkOption
@ -6,6 +12,7 @@ let
; ;
cfg = config; cfg = config;
in in
{ {
options.collections = mkOption { options.collections = mkOption {
description = '' description = ''
@ -25,46 +32,62 @@ in
} }
``` ```
''; '';
type = with types; attrsOf (submodule ({ name, config, ... }: { type =
options = { with types;
type = mkOption { attrsOf (
description = "Type of entries in the collection"; submodule (
type = types.deferredModule; { name, config, ... }:
}; {
name = mkOption { options = {
description = "Symbolic name, used as a human-readable identifier"; type = mkOption {
type = types.str; description = "Type of entries in the collection";
default = name; type = types.deferredModule;
}; };
prefixes = mkOption { name = mkOption {
description = '' description = "Symbolic name, used as a human-readable identifier";
List of historic output locations for files in the collection 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. The first element is the canonical location.
All other elements are used to create redirects to 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. 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. 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; type = with types; nonEmptyListOf str;
example = [ "." ]; example = [ "." ];
default = [ config.name ]; default = [ config.name ];
}; };
entry = mkOption { entry = mkOption {
description = "An entry in the collection"; description = "An entry in the collection";
type = with types; collection (submodule ({ type =
imports = [ config.type ]; with types;
_module.args.collection = config; collection (submodule ({
process-locations = ls: with lib; concatMap (l: map (p: "${p}/${l}") config.prefixes) ls; 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; by-name = mkOption {
default = with lib; listToAttrs (map (e: { name = e.name; value = e; }) config.entry); 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 = config.files =

View file

@ -1,10 +1,14 @@
{ config, options, lib, pkgs, ... }: {
config,
options,
lib,
...
}:
let let
inherit (lib) inherit (lib)
mkOption mkOption
types types
; ;
cfg = config;
in in
{ {
imports = lib.nixFiles ./.; imports = lib.nixFiles ./.;
@ -14,68 +18,81 @@ in
type = with types; attrsOf deferredModule; type = with types; attrsOf deferredModule;
}; };
config.content-types.document = { name, config, options, link, ... }: { config.content-types.document =
config._module.args.link = config.link; {
options = { name,
name = mkOption { config,
description = "Symbolic name, used as a human-readable identifier"; options,
type = types.str; link,
default = name; ...
}; }:
locations = mkOption { {
description = '' config._module.args.link = config.link;
List of historic output locations for the resulting file 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. Elements are relative paths to output files, without suffix.
The suffix will be added depending on output file type. The suffix will be added depending on output file type.
The first element is the canonical location. The first element is the canonical location.
All other elements are used to create redirects to 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. 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. 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; type = with types; nonEmptyListOf str;
apply = config.process-locations; apply = config.process-locations;
example = [ "about/overview" "index" ]; example = [
default = [ config.name ]; "about/overview"
}; "index"
process-locations = mkOption { ];
description = "Function to post-process the output locations of contained document"; default = [ config.name ];
type = types.functionTo options.locations.type; };
default = lib.id; process-locations = mkOption {
}; description = "Function to post-process the output locations of contained document";
link = mkOption { type = types.functionTo options.locations.type;
description = "Helper function for transparent linking to other pages"; default = lib.id;
type = with types; functionTo attrs; };
# TODO: we may want links to other representations, link = mkOption {
# and currently the mapping of output types to output file description = "Helper function for transparent linking to other pages";
# names is soft. type = with types; functionTo attrs;
default = with lib; target: # TODO: we may want links to other representations,
let # and currently the mapping of output types to output file
path = relativePath (head config.locations) (head target.locations); # names is soft.
links = mapAttrs default =
(type: output: with lib;
path + optionalString (type != "") ".${type}" target:
let
path = relativePath (head config.locations) (head target.locations);
links = mapAttrs (
type: _output: path + optionalString (type != "") ".${type}"
# ^^^^^^^^^^^^ # ^^^^^^^^^^^^
# convention for raw files # convention for raw files
) ) target.outputs;
target.outputs; in
in if length (attrValues links) == 0 then
if length (attrValues links) == 0 throw "no output to link to for '${target.name}'"
then throw "no output to link to for '${target.name}'" else if length (attrValues links) == 1 then
else if length (attrValues links) == 1 links
then links // { // {
__toString = _: head (attrValues links); __toString = _: head (attrValues links);
} }
else links; else
}; links;
outputs = mkOption { };
description = '' outputs = mkOption {
Representations of the document in different formats description = ''
''; Representations of the document in different formats
type = with types; attrsOf (either attrs pathInStore); '';
type = with types; attrsOf (either attrs pathInStore);
};
}; };
}; };
};
} }

View file

@ -1,4 +1,9 @@
{ config, options, lib, ... }: {
config,
options,
lib,
...
}:
let let
inherit (lib) inherit (lib)
mkOption mkOption
@ -7,75 +12,85 @@ let
cfg = config; cfg = config;
in in
{ {
content-types.event = { config, collection, ... }: { content-types.event =
imports = [ cfg.content-types.page ]; { config, collection, ... }:
options = { {
collection = mkOption { imports = [ cfg.content-types.page ];
description = "Collection this event belongs to"; options = {
type = options.collections.type.nestedTypes.elemType; collection = mkOption {
default = collection; 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;
};
}; };
start-date = mkOption { config.name = with lib; mkDefault (slug config.title);
description = "Start date of the event"; config.summary = lib.mkDefault config.description;
type = with types; str; config.outputs.html = lib.mkForce (
}; (cfg.templates.html.page config).override (
start-time = mkOption { _final: prev: {
description = "Start time of the event"; html.body.content =
type = with types; str; with lib;
default = null; map (
}; e:
end-date = mkOption { if isAttrs e && e ? section then
description = "End date of the event"; recursiveUpdate e {
type = with types; str; section.content = [
default = null; {
}; dl.content =
end-time = mkOption { [
description = "End time of the event"; {
type = with types; str; terms = [ { dt = "Location"; } ];
default = null; descriptions = [ { dd = config.location; } ];
}; }
location = mkOption { {
description = "Location of the event"; terms = [ { dt = "Start"; } ];
type = with types; str; descriptions = [
}; {
}; dd = config.start-date + lib.optionalString (!isNull config.start-time) " ${config.start-time}";
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 ++ lib.optional (!isNull config.end-date) {
(e: terms = [ { dt = "End"; } ];
if isAttrs e && e ? section descriptions = [
then {
recursiveUpdate e dd = config.end-date + lib.optionalString (!isNull config.end-time) " ${config.end-time}";
{ }
section.content = [ ];
{ };
dl.content = [ }
{ ] ++ e.section.content;
terms = [{ dt = "Location"; }]; }
descriptions = [{ dd = config.location; }]; else
} e
{ ) prev.html.body.content;
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;
})); }
}; )
);
};
} }

View file

@ -1,19 +1,26 @@
{ config, options, lib, ... }: {
config,
options,
lib,
...
}:
let let
inherit (lib) inherit (lib)
mkOption mkOption
types types
; ;
cfg = config; cfg = config;
subtype = baseModule: types.submodule [ subtype =
baseModule baseModule:
{ types.submodule [
_module.freeformType = types.attrs; baseModule
# XXX: this is supposed to be used with a finished value, {
# and we don't want to process locations again. _module.freeformType = types.attrs;
process-locations = lib.mkForce lib.id; # 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 in
{ {
options.menus = mkOption { options.menus = mkOption {
@ -23,50 +30,59 @@ in
type = with types; attrsOf (submodule config.content-types.navigation); type = with types; attrsOf (submodule config.content-types.navigation);
}; };
config.content-types.named-link = { ... }: { config.content-types.named-link =
options = { { ... }:
label = mkOption { {
description = "Link label"; options = {
type = types.str; label = mkOption {
}; description = "Link label";
url = mkOption { type = types.str;
description = "Link URL"; };
type = types.str; url = mkOption {
}; description = "Link URL";
}; type = types.str;
}; };
};
config.content-types.navigation = { name, config, ... }: { };
options = {
name = mkOption { config.content-types.navigation =
description = "Symbolic name, used as a human-readable identifier"; { name, config, ... }:
type = types.str; {
default = name; options = {
}; name = mkOption {
label = mkOption { description = "Symbolic name, used as a human-readable identifier";
description = "Menu label"; type = types.str;
type = types.str; default = name;
default = name; };
}; label = mkOption {
items = mkOption { description = "Menu label";
description = "List of menu items"; type = types.str;
type = with types; listOf (attrTag { default = name;
menu = mkOption { type = submodule cfg.content-types.navigation; }; };
page = mkOption { type = subtype cfg.content-types.page; }; items = mkOption {
link = mkOption { type = submodule cfg.content-types.named-link; }; description = "List of menu items";
}); type =
}; with types;
outputs = mkOption { listOf (attrTag {
description = '' menu = mkOption { type = submodule cfg.content-types.navigation; };
Representations of the navigation structure in different formats page = mkOption { type = subtype cfg.content-types.page; };
link = mkOption { type = submodule cfg.content-types.named-link; };
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); outputs = mkOption {
default.html = page: cfg.templates.html.nav { description = ''
menu = config; inherit page; 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;
};
}; };
}; };
}; };
};
} }

View file

@ -17,36 +17,38 @@ in
config.files = with lib; cfg.templates.files (attrValues config.pages); config.files = with lib; cfg.templates.files (attrValues config.pages);
config.content-types.page = { name, config, ... }: { config.content-types.page =
imports = [ cfg.content-types.document ]; { name, config, ... }:
options = { {
title = mkOption { imports = [ cfg.content-types.document ];
description = "Page title"; options = {
type = types.str; title = mkOption {
default = name; 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;
};
}; };
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.outputs.html = cfg.templates.html.page config;
}; };
config.templates.html.page = lib.template cfg.templates.html.dom (page: { config.templates.html.page = lib.template cfg.templates.html.dom (page: {
html = { html = {

View file

@ -4,14 +4,35 @@ let
inherit (import ./. { }) lib; inherit (import ./. { }) lib;
in in
{ {
test-relativePath = with lib; test-relativePath =
with lib;
let let
testData = [ testData = [
{ from = "bar"; to = "baz"; expected = "./baz"; } {
{ from = "foo/bar"; to = "foo/baz"; expected = "./baz"; } from = "bar";
{ from = "foo"; to = "bar/baz"; expected = "./bar/baz"; } to = "baz";
{ from = "foo/bar"; to = "baz"; expected = "./../baz"; } expected = "./baz";
{ from = "foo/bar/baz"; to = "foo"; expected = "./../../foo"; } }
{
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 in
{ {