{
  lib,
  prefix,
  name,
  config,
  options,
  ...
}:
let
  cfg = config._page;

  contentType = lib.types.coercedTo lib.types.path (file: { inherit file; }) (
    lib.types.attrTag {
      file = lib.mkOption {
        description = "A markdown file.";
        type = lib.types.path;
      };
      text = lib.mkOption {
        description = "Lines of markdown.";
        type = lib.types.lines;
      };
      functions = lib.mkOption {
        description = "Nix file to scan for RFC145 doc comments.";
        type = lib.types.submodule {
          options.file = lib.mkOption {
            type = lib.types.path;
            description = "Nix file.";
          };
          options.loc = lib.mkOption {
            type = lib.types.listOf lib.types.str;
            default = if lib.lists.hasPrefix [ "lib" ] cfg.loc then builtins.tail cfg.loc else cfg.loc;
            defaultText = lib.literalMD ''
              `loc`'s attrpath, without any leading "lib"
            '';
            description = ''
              Optional attrpath where functions are defined.
              Provided to `nixdoc` as `--category`.

              Will scan `lib` for attribute locations in the functions set at this attrpath.

              Used in conjunction with `nix`.
            '';
          };
        };
      };
      options = lib.mkOption {
        description = ''
          Set of options or list of option docs-templates.

          If an attrset is provided, it will be coerced using `lib.options.optionAttrSetToDocList`.
        '';
        type = lib.types.raw;
        apply = opts: if builtins.isAttrs opts then lib.options.optionAttrSetToDocList opts else opts;
      };
    }
  );
in
{
  options._page = {
    loc = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      description = "Page's location in the menu.";
      default = prefix ++ [ name ];
      defaultText = lib.literalExpression "prefix ++ [ name ]";
      readOnly = true;
    };
    target = lib.mkOption {
      type = lib.types.str;
      default = lib.optionalString (cfg.content != [ ]) (
        lib.concatStringsSep "/" (cfg.loc ++ [ "index.md" ])
      );
      defaultText = lib.literalMD ''
        `""` if page has no content, otherwise a filepath derived from the page's `loc`.
      '';
      description = "Where to render content and link menu entries.";
    };
    title = lib.mkOption {
      type = lib.types.nullOr lib.types.str;
      default = null;
      description = "Page's heading title.";
    };
    content = lib.mkOption {
      type = lib.types.listOf contentType;
      default = [ ];
      description = "Optional content sections rendered after the title.";
      # Give each section a unique ID, based on `loc`
      apply = lib.imap0 (
        i: section:
        section
        // {
          id = lib.concatMapStringsSep "." toString (cfg.loc ++ [ i ]);
        }
      );
    };
    toMenu = lib.mkOption {
      type = lib.types.functionTo lib.types.str;
      description = ''
        A function to render the menu for this sub-tree.

        Typically, this involves invoking `_page.toMenu` for all children.

        **Inputs**

        `settings`
        : `nested`
          : Whether this menu category supports nesting.

          `indent`
          : The indentation to use before non-empty lines.

          `page`
          : This page node.

          `prefix`
          : The menu loc prefix, to be omitted from menu entry text.
            Usually the `loc` of the parent page node.
      '';
    };
    children = lib.mkOption {
      type = lib.types.ints.unsigned;
      description = ''
        The number of child pages.
      '';
      readOnly = true;
    };
    pages = lib.mkOption {
      type = lib.types.listOf lib.types.raw;
      description = "This page, its children, their children, etc.";
      readOnly = true;
    };
  };

  config._page = {
    toMenu = import ./to-menu.nix {
      inherit lib;
      optionNames = builtins.attrNames options;
    };

    pages = lib.pipe options [
      builtins.attrNames
      (removeAttrs config)
      builtins.attrValues
      (lib.concatMap (x: x._page.pages))
      (children: [ cfg ] ++ children)
    ];
  };
}
