Skip to main content

Automatic Firefox Bookmarks for Self-Hosted Services on NixOS

·1250 words·6 mins
William Vandervalk
Author
William Vandervalk

One of my favourite aspects of self-hosting with NixOS is how simple it is to deploy a multitude of services using the built-in modules, and as a result, I deploy many services.

Unfortunately, it can become quite a headache trying to keep track of so many different services all potentially running on different hosts, with different URLs and ports.

To help with this, I have tended to configure my Firefox bookmarks using the home-manager module, but it is still a somewhat tedious process to manually configure each one.

Fortunately, using the power of flakes and home-manager along with a little bit of Nix magic, it is possible to automatically generate a bookmarks folder for Firefox containing all my Nix based services, without having to manually configure anything!

Configuring an example service
#

To demonstrate how this works, here is a typical example of a web service configuration (Forgejo in this case) including the nginx reverse proxy configuration:

{config, ...}: {
  services.forgejo = {
    enable = true;
    settings = {
      server = {
        DOMAIN = "git.williamvandervalk.com";
        ROOT_URL = "https://git.williamvandervalk.com";
        HTTP_PORT = 3000;
      };
    };
  };

  services.nginx.virtualHosts."git.williamvandervalk.com" = {
    locations."/".proxyPass = "http://localhost:3000";
  };
}

Home-manager Firefox configuration
#

The goal is to create a bookmark in the home-manager Firefox configuration, that links to our service.

Normally, bookmarks are configured manually in home-manager configuration as follows:

{
  programs.firefox = {
    enable = true;
    profiles.default = {
      bookmarks = {
        force = true;
        settings = {
          toolbar = true;
          bookmarks = [
            {
              name = "Services";
              bookmarks =
                [
                  {
                    name = "ArgoCD";
                    url = "https://argocd.williamvandervalk.com";
                  }
                  {
                    name = "Ceph";
                    url = "https://ceph.williamvandervalk.com";
                  }
                  {
                    name = "Git";
                    url = "https://git.williamvandervalk.com";
                  }
                ]
            }
          ];
        };
      };
    };
  };
}

This will create a bookmarks folder in the bookmarks toolbar named “Services”, containing the “ArgoCD”, “Ceph” and “Git” bookmarks.

In my case, ArgoCD and Ceph are not hosted using NixOS but will remain as examples of custom bookmarks.

Passing the flake as argument to home-manager
#

To automate this process, it is necessary to somehow collect all instances of config.services.nginx.virtualHosts."service.williamvandervalk.com" configured in the flake.

To do this, simply pass the flake itself (i.e. self) from flake.nix as an extraSpecialArg to home-manager.

This will make it possible to use self.outputs.nixosConfigurations.<host>.config.services.nginx.virtualHosts to parse the virtualHosts configured for a specific host in the same flake.

flake.nix
#

{
  description = "NixOS configuration";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
    home-manager = {
      url = "github:nix-community/home-manager/release-25.05";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    inputs@{
      nixpkgs,
      home-manager,
      self,
      ...
    }:
    {
      nixosConfigurations = {
        hostname = nixpkgs.lib.nixosSystem {
          system = "x86_64-linux";
          modules = [
            ./configuration.nix
            home-manager.nixosModules.home-manager
            {
              home-manager = {
                useGlobalPkgs = true;
                useUserPackages = true;
                users.jdoe = ./home.nix;

                # Pass flake outputs to home-manager
                extraSpecialArgs = {
                  inherit inputs self;
                };
              };
            }
          ];
        };
      };
    };
}

Automatically creating bookmarks
#

Now that the flake itself has been passed as an argument to home-manager, it is possible to generate the list of bookmarks for all services that have a corresponding nginx virtualHost automatically using the following Nix expression:

{
  lib,
  self,
  ...
}:
{
  programs.firefox = {
    enable = true;
    profiles.default = {
      bookmarks = {
        force = true;
        settings = {
          toolbar = true;
          bookmarks = [
            {
              name = "Services";
              bookmarks =
                [
                  {
                    name = "ArgoCD";
                    url = "https://argocd.williamvandervalk.com";
                  }
                  {
                    name = "Ceph";
                    url = "https://ceph.williamvandervalk.com";
                  }
                ]
                ++
                # Create bookmarks for all nginx virtualHosts in this flake
                lib.mapAttrsToList
                  (virtualHost: _virtualHostConfig: {
                    name = virtualHost;
                    url = "https://${virtualHost}";
                  })
                  (
                    lib.concatMapAttrs (
                      _host: osConfig: osConfig.config.services.nginx.virtualHosts
                    ) self.outputs.nixosConfigurations
                  );
            }
          ];
        };
      };
    };
  };
}
This process will only work for virtualHosts that use the same descriptive subdomain pattern (i.e. “service.example.com”).

Here is the result of the initial expression:

“Firefox bookmarks menu”

As you can see, it is functional, but not exactly elegant. Notice that it successfully picked up the configured Forgejo instance, as well as several other virtualHosts, including a Minio instance that I have configured.

Explanation
#

Breaking down what is going on in the previous Nix expression, there is an initial list of manually defined bookmarks:

...
[
  {
    name = "ArgoCD";
    url = "https://argocd.williamvandervalk.com";
  }
  {
    name = "Ceph";
    url = "https://ceph.williamvandervalk.com";
  }
]
...

The attribute set of all virtualHosts configured for all hosts in the flake is created using:

...
(
  lib.concatMapAttrs (
    _host: osConfig: osConfig.config.services.nginx.virtualHosts
  ) self.outputs.nixosConfigurations
);
...

This is used as the input for lib.mapAttrsToList which constructs the list items corresponding to each virtualHost. The resulting list is then concatenated to the list of manually defined bookmarks:

...
[
  {
    name = "ArgoCD";
    url = "https://argocd.williamvandervalk.com";
  }
  {
    name = "Ceph";
    url = "https://ceph.williamvandervalk.com";
  }
]
++
lib.mapAttrsToList
  (virtualHost: _virtualHostConfig: {
    name = virtualHost;
    url = "https://${virtualHost}";
  })
  (
    lib.concatMapAttrs (
      _host: osConfig: osConfig.config.services.nginx.virtualHosts
    ) self.outputs.nixosConfigurations
  );
...

Cleaning up result
#

Trimming domain from names
#

The previous result is functional, but a little bit ugly. To improve it, lib.strings.removeSuffix can be used to trim the domains from the bookmark names:

{
  lib,
  self,
  ...
}:
{
  programs.firefox = {
    enable = true;
    profiles.default = {
      bookmarks = {
        force = true;
        settings = {
          toolbar = true;
          bookmarks = [
            {
              name = "Services";
              bookmarks =
                [
                  {
                    name = "ArgoCD";
                    url = "https://argocd.williamvandervalk.com";
                  }
                  {
                    name = "Ceph";
                    url = "https://ceph.williamvandervalk.com";
                  }
                ]
                ++
                # Create bookmarks for all nginx virtualHosts in this flake
                lib.mapAttrsToList
                  (virtualHost: _virtualHostConfig: {
                    name = lib.strings.removeSuffix ".williamvandervalk.com" virtualHost;
                    url = "https://${virtualHost}";
                  })
                  (
                    lib.concatMapAttrs (
                      _host: osConfig: osConfig.config.services.nginx.virtualHosts
                    ) self.outputs.nixosConfigurations
                  );
            }
          ];
        };
      };
    };
  };
}

Here is the result of the improved expression, after trimming domains from the bookmark names:

“Firefox bookmarks menu after removing domain from names”

Filtering out unwanted virtualHosts
#

This is an improved result, but there are still some extra bookmarks (e.g. the root domain, “www”) that might be nice to remove, which is achieved using builtins.removeAttrs:

{
  lib,
  self,
  ...
}:
{
  programs.firefox = {
    enable = true;
    profiles.default = {
      bookmarks = {
        force = true;
        settings = {
          toolbar = true;
          bookmarks = [
            {
              name = "Services";
              bookmarks =
                [
                  {
                    name = "ArgoCD";
                    url = "https://argocd.williamvandervalk.com";
                  }
                  {
                    name = "Ceph";
                    url = "https://ceph.williamvandervalk.com";
                  }
                ]
                ++
                # Create bookmarks for all nginx virtualHosts in this flake
                lib.mapAttrsToList
                  (virtualHost: _virtualHostConfig: {
                    name = lib.strings.removeSuffix ".williamvandervalk.com" virtualHost;
                    url = "https://${virtualHost}";
                  })
                  (
                    builtins.removeAttrs
                      (lib.concatMapAttrs (
                        _host: osConfig: osConfig.config.services.nginx.virtualHosts
                      ) self.outputs.nixosConfigurations)
                      [
                        "default"
                        "localhost"
                        "s3.williamvandervalk.com"
                        "williamvandervalk.com"
                        "www.williamvandervalk.com"
                      ]
                  );
            }
          ];
        };
      };
    };
  };
}

Here is the result of the improved expression, after filtering unwanted virtualHosts:

“Firefox bookmarks menu after filtering links”

Capitalizing bookmark names
#

Almost perfect! To polish it a little more, define a custom function that will capitalize the first character of each name:

{
  lib,
  self,
  ...
}:
let
  # Helper function to capitalize the first character of a string
  capitalize =
    str: lib.toUpper (builtins.substring 0 1 str) + (lib.removePrefix (builtins.substring 0 1 str) str);
in
{
  programs.firefox = {
    enable = true;
    profiles.default = {
      bookmarks = {
        force = true;
        settings = {
          toolbar = true;
          bookmarks = [
            {
              name = "Services";
              bookmarks =
                [
                  {
                    name = "ArgoCD";
                    url = "https://argocd.williamvandervalk.com";
                  }
                  {
                    name = "Ceph";
                    url = "https://ceph.williamvandervalk.com";
                  }
                ]
                ++
                # Create bookmarks for all nginx virtualHosts in this flake
                lib.mapAttrsToList
                  (virtualHost: _virtualHostConfig: {
                    name = capitalize (lib.strings.removeSuffix ".williamvandervalk.com" virtualHost);
                    url = "https://${virtualHost}";
                  })
                  (
                    builtins.removeAttrs
                      (lib.concatMapAttrs (
                        _host: osConfig: osConfig.config.services.nginx.virtualHosts
                      ) self.outputs.nixosConfigurations)
                      [
                        "williamvandervalk.com"
                        "www.williamvandervalk.com"
                      ]
                  );
            }
          ];
        };
      };
    };
  };
}

Here is the final result:

“Firefox bookmarks menu after capitalizing names”