userdir-publish#
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
userdir-publish==<version>
Configuration#
Each layer overrides the previous one:
built-in defaults,
the user config,
$XDG_CONFIG_HOME/userdir-publish/config.tomlor$XDG_CONFIG_HOME/userdir-publish.toml,[tool.userdir-publish]in the project’spyproject.toml,
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.
[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.
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: setuseror pass--user research-group.A setgid, group-owned directory inside one user’s web directory: set
permissionstogroup-writable.An Apache
Aliasto a shared directory: setweb-rootto that directory.
Command-line interface#
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"]