userdir-publish#

PyPI package Supported Python versions CI status License

userdir-publish copies a built static website to a personal web directory, such as https://web.example.edu/~jdoe/, that Apache serves with mod_userdir. It runs rsync over SSH, or to a local path when the web directory is mounted or you are on the server (see transport). It removes stale files, sets permissions the web server can read, and refuses a build that lacks index.html or the current .htaccess rules from htaccess-source.

Installation#

rsync must be available locally and on the server. On macOS, install GNU rsync (for instance with brew install rsync), because the built-in openrsync ignores the permission settings. Install the tool from PyPI:

uv tool install userdir-publish
pipx install userdir-publish
pip install userdir-publish

or pin it in a project, so that everyone publishes with the same version:

uv add --dev userdir-publish
pixi add --pypi userdir-publish
requirements-dev.txt#
userdir-publish==<version>

Configuration#

Each layer overrides the previous one:

  1. built-in defaults,

  2. the user config, $XDG_CONFIG_HOME/userdir-publish/config.toml or $XDG_CONFIG_HOME/userdir-publish.toml,

  3. [tool.userdir-publish] in the project’s pyproject.toml,

  4. command-line flags.

userdir-publish --show-config prints the merged result. The user config accepts the project options at its top level as personal defaults.

Options#

Generated from the settings schema; hover over an option for its description.

pyproject.toml#
[tool.userdir-publish]
host = "web.example.edu"
htaccess-source = "docs"
keep = []
permissions = "private"
source = "docs/_build/html"
subdirectory = ""
transport = "auto"
url = "https://web.example.edu/~{user}/{subdirectory}/"
user = "jdoe"
web-root = "~/public_html"

source is required, here or as the first command-line argument. keep takes rsync filter patterns, and permissions also accepts a raw rsync --chmod specification. private removes all group access; group-writable suits group pages. htaccess-source is searched for .htaccess files outside hidden directories and, in a Git work tree, outside Git-ignored paths. With a host, the printed url defaults to https://<host>/~<user>/<subdirectory>/, where <user> is the login name, so set it only for servers that serve the pages under a different address.

Login name#

The login name is resolved in this order: --user, a matching entry under [hosts] in the user config, user in the project (for a shared account), user at the top level of the user config, and finally the local $USER.

~/.config/userdir-publish/config.toml#
user = "jdoe"

[hosts."*.example.edu"]
user = "jdoe-rub"

[hosts."*"]
user = "other-login"

Host keys are case-sensitive shell-style patterns (*, ?, [abc]) matched against the configured host, without resolving SSH aliases. An exact key wins over patterns; otherwise the first matching pattern in file order applies, so put * last.

Homepage with subprojects#

A homepage published to the web-root would delete the subdirectory targets of other projects. Protect every top-level directory with keep:

[tool.userdir-publish]
keep = ["/*/"]

Group pages#

mod_userdir serves only user directories, so a group page needs one of these server setups:

  • A shared account, such as ~research-group, that has your SSH key: set user or pass --user research-group.

  • A setgid, group-owned directory inside one user’s web directory: set permissions to group-writable.

  • An Apache Alias to a shared directory: set web-root to that directory.

Command-line interface#

userdir-publish Usage: userdir-publish [OPTIONS] [source] Publish a built website to a web directory served by Apache's mod_userdir. Options are read from the user config, then [tool.userdir-publish] in pyproject.toml, then these flags, each overriding the previous. ╭─ Arguments ──────────────────────────────────────────────────────────────────────────╮ │source<str>Directory with the built website.│ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Options ────────────────────────────────────────────────────────────────────────────╮ │--host<str>SSH host that serves the pages.│ │--user<str>Login name on the host.│ │--web-root<str>Web directory, may contain       │ │{user}.                          │ │--subdirectory<str>Subdirectory of the web root.│ │--keep<str>rsync pattern under the target   │ │that is never deleted.           │ │--permissions<str>private, group-writable, none, or│ │an rsync --chmod spec.           │ │--transport<str>auto, local, or ssh.│ │--dry-run--no-dry-runShow what would change without   │ │copying.                         │ │[default: no-dry-run]            │ │--progress--no-progressShow the transfer progress.      │ │Default: if the output is a      │ │terminal.                        │ │--show-config--no-show-configPrint the merged configuration   │ │and exit.                        │ │[default: no-show-config]        │ │--install-completionInstall completion for the       │ │current shell.                   │ │--show-completionShow completion for the current  │ │shell, to copy it or customize   │ │the installation.                │ │--helpShow this message and exit.│ ╰──────────────────────────────────────────────────────────────────────────────────────╯
userdir-publish Usage: userdir-publish [OPTIONS] [source] Publish a built website to a web directory served by Apache's mod_userdir. Options are read from the user config, then [tool.userdir-publish] in pyproject.toml, then these flags, each overriding the previous. ╭─ Arguments ──────────────────────────────────────────────────────────────────────────╮ │source<str>Directory with the built website.│ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Options ────────────────────────────────────────────────────────────────────────────╮ │--host<str>SSH host that serves the pages.│ │--user<str>Login name on the host.│ │--web-root<str>Web directory, may contain       │ │{user}.                          │ │--subdirectory<str>Subdirectory of the web root.│ │--keep<str>rsync pattern under the target   │ │that is never deleted.           │ │--permissions<str>private, group-writable, none, or│ │an rsync --chmod spec.           │ │--transport<str>auto, local, or ssh.│ │--dry-run--no-dry-runShow what would change without   │ │copying.                         │ │[default: no-dry-run]            │ │--progress--no-progressShow the transfer progress.      │ │Default: if the output is a      │ │terminal.                        │ │--show-config--no-show-configPrint the merged configuration   │ │and exit.                        │ │[default: no-show-config]        │ │--install-completionInstall completion for the       │ │current shell.                   │ │--show-completionShow completion for the current  │ │shell, to copy it or customize   │ │the installation.                │ │--helpShow this message and exit.│ ╰──────────────────────────────────────────────────────────────────────────────────────╯

Task runners#

Publishing is separate from building. Chain them with a Poe the Poet sequence task:

[tool.poe.tasks.website]
sequence = ["doc", { cmd = "userdir-publish" }]

or with Pixi:

[tasks.website]
cmd = "userdir-publish"
depends-on = ["doc"]