Repository navigation
v0.2.0
Upgrade notes below — a dependency that moved, renamed CSS classes, an app that
must change position in INSTALLED_APPS — address projects built against an
earlier version of the package. A fresh install needs none of them.
Added
- Shared blocks can now be pinned to a site-wide slot (
Block.site_slot: head/body
anchors in the page template) and render automatically on every page of a site that
fills in their shared content — no per-page placement needed; combined with
page-type restrictions it scopes to every page of those types. A page that places
the block itself takes over (its own position and variant, or a "hide on this page"
toggle), variants cascade page → site (SharedBlock.variant) → block default,
missing locales fall back to the default locale's content, and
Block.render_in_previewkeeps tracking scripts out of previews and screenshots.
The streams v1 API, the MCP studio tools, and the cross-project push envelope all
carry the new fields. - Platform menus: an editor writes a site's navigation in the Wagtail admin, scoped
to a site and a language, under a new Platform section. Entries point at a page, a
named route, an address elsewhere, or a group of those. The menu renders along the
top of the dashboard and inside the small-screen drawer, with dropdowns,
separators, and the entry for the page being read marked as current; unpublished
pages are shown only to editors, linked to their draft. A site with no menu written
for it shows no bar and no drawer section. Menus are also readable and writable
over/api/dashboard/v1/menus/and through thephoxtail_dashboard_*MCP tools,
so an agent can list, create, update and delete them — updates take anIf-Match
ETag, and an entry type the menu does not define is refused rather than silently
dropped. - Dashboard widgets are declarative: give
add_widget()a title, description, icon
andurl_nameand phoxtail renders the card, so an app no longer ships a template
and a context function for a link. Cards are grouped under a heading per app
(DashboardModule(verbose_name=...)), and the Account group opens with a card for
the Wagtail admin, shown only to users who may reach it. A site can amend what
other apps contribute —registry.get_module(),unregister(),
DashboardModule.remove_widget()andremove_nav_item()— override the index view
itself through the newPHOXTAIL_DASHBOARD_VIEWsetting, and shadow any phoxtail
template from a project-leveltemplates/directory, which hatched projects now
get. - The dashboard is served under a language prefix (
/en/dashboard/) and carries a
language switcher offering every language the project serves. A language with no
menu written for it still opens the platform — the menu bar is simply absent, and
the dashboard's own screens are translated regardless. UrlMountacceptsi18n=True, mounting an app's URLs under a language prefix
(/en/shop/). Set it for anything a person reads; machine endpoints stay
unprefixed so their callers need not learn a locale. When at least one app opts in,
Django'sset_languageview is published ati18n/alongside it.phoxtail hatchandphoxtail installtake a--sourceoption that says where a
package is resolved from, using uv's own locator syntax:git+ssh://…@refor
git+https://…@tagfor a repository, a directory for a local checkout (added as an
editable path), or a.whl/.tar.gzURL for a built artefact. PyPI stays the
default and nothing changes without the flag. Both commands check a non-default
source before writing anything, and neither treats an unanswered check as a
failure: with no network the check is skipped and uv, which can resolve from its
own cache, has the final say.phoxtail upgrade phoxtailnow compares the project's phoxtail-owned declarations
inpyproject.tomlagainst the current project template and offers to apply the
difference before locking. Previously it only advanced the lockfile, so a project
kept declaring whatever extras it was hatched with even after the template moved a
dependency to a different one. Projects hatched before the dependency-groups split
get the[dependency-groups]table created for them.TRADEMARK.mdsets out the permitted use of the Phoxtail name, wordmark and logo,
which the BSD licence does not grant. Saying a product is built with Phoxtail needs
no permission; naming a product after it does.SECURITY.md— how to report a vulnerability privately.
Changed
-
Wagtail 8.0 is now the supported version, raising the
engineextra from
wagtail>=7.4.2,<8.0towagtail>=8.0,<9.0. Django stays at<6.0and the Python
floor stays at 3.11: Wagtail 8 runs on Django 5.2, so neither has to move yet.
wagtailmediais raised to>=0.18.1, which carries a permission fix for the
media chooser — it previously exposed a media item's id, title and edit URL to
users without access to it.Two Wagtail 8 changes are worth knowing about. Image renditions no longer convert
AVIF and WebP to PNG, so sites serving those formats will now emit them directly;
setWAGTAILIMAGES_FORMAT_CONVERSIONSto restore the old behaviour. And
menu_orderis now respected between viewsets inside aViewSetGroup, which
reorders some admin menus. -
Breaking for projects built against an earlier version.
playwrightand
Pilloware no longer installed by default. They moved to a newstudioextra,
which carries the screenshot tools inphoxtail.mcp.studioand nothing else — a
base install no longer pulls a browser stack it may never use.Newly hatched projects get
phoxtail[studio]in theirdevdependency group, and
projects still on thephoxtail[dev]alias keep it because that alias now includes
studio. Projects that already moved to thedev-tools+ dependency-group layout
must add it themselves —phoxtail upgradeadvances the lockfile without rewriting
pyproject.toml:[dependency-groups] dev = [ "phoxtail[dev-tools]", "phoxtail[studio]", ]
Then run
uv lock; the Dockerfile syncs with--frozenand refuses a lockfile
that no longer matches. Without the entry the development image fails at
playwright install. Production images are unaffected, and lose a browser stack
they were never using. -
htmx is no longer bundled with phoxtail. It now comes from
django-htmx, which
phoxtail already depends on, via its{% htmx_script %}tag — one copy instead of
two, at 2.0.10 instead of 2.0.8. Any project template that loads htmx with
<script src="{% static 'phoxtail_core/js/htmx.min.js' %}">must switch to
{% htmx_script %}; the static file is gone, and under
ManifestStaticFilesStoragea stale reference raises at render time. -
Every declared dependency floor now names a version phoxtail is tested against,
replacing inherited minimums that had never been installed — several of which did
not work. Installing phoxtail alongside an older pinned dependency will now fail to
resolve rather than fail at import. -
phoxtail installno longer decides on its own whether a package belongs in
INSTALLED_APPS. It previously treated any importable top-level module as a Django
app, so installing something likehttpxorredisregistered it and left Django
importing a non-app at startup. A package now has to carry anAppConfig, models,
or migrations to be considered one — and because no inspection can rule out a
Django app carrying none of those, the result is offered as the default answer
rather than acted on. Answer no and nothing is registered; answer yes for a package
that was not detected and you are asked for the module name.--app <module>and
--no-appanswer ahead of time, and a non-interactive run acts on the detection
result without prompting. The module name is read from the installed
distribution's metadata rather than guessed from the package name, so apps whose
names differ —django-taggitinstallstaggit— are registered instead of
silently skipped. The stream-population step follows the same answer: declining
registration skips it instead of running it against an app Django does not know
about. -
The profile view is borderless: every control — the content panel, the
account-settings links, the personal-information rows, the Edit button, the avatar
and the file field — is now a shape defined by its fill against the fill behind it,
in both colour schemes. The information rows are tiles rather than lines, the two
columns share one fill, the page heading has lost its panel and the icon beside its
subtitle, buttons are pills on the core button's metrics, and the row icons are the
dashboard's widget icon. The stylesheet that dressed it moved with it — see
Removed. -
The annotations shipped under
py.typedare now verified rather than asserted —
the package type-checks clean, and CI fails on any error. Two signatures were
corrected against their callers in the process:collection_idon the studio
client'screate_variantacceptsNone, andProvidersubclasses now declare
their one-argument constructor on the base class. -
The
agentapp's migration0003was folded into0001: it set a model's
verbose_nameand nothing else, so it emitted no SQL. No action required — a
database that already applied it is unchanged, and Django ignores the leftover
history row. -
LICENSEnames the copyright holder as a person rather than a trade name.
Removed
-
The dashboard stylesheet
phoxtail_dashboard/css/ui.cssis gone. Only the profile
view rendered its components, yet every dashboard page linked it. Its rules now
live inusers/css/profile.cssunder ausr-prefix, and its two genuinely shared
rules,.dash-pageand.dash-page-inner, moved into
phoxtail_dashboard/css/dashboard.css. A site using any of these class names must
supply its own styles or switch to theusr-equivalents:- Owned by the profile view, now
usr--prefixed:dash-page-header*,
dash-page-content,dash-columns*,dash-column,dash-action-*,
dash-info-*,dash-btn*,dash-avatar*,dash-profile-*,dash-container,
dash-section,dash-hidden,dash-md-flex,dash-md-hide,dash-mb-6,
dash-mt-4. - Widget cards, dropped outright:
.dash-widgets-grid,.dash-widget-card,
.dash-widget-title,.dash-widget-link,.dash-widget-header,
.dash-widget-list*,.dash-widget-cta,.dash-widget-empty*. Widget cards no
longer carry a statistic or a detail line either, so.dash-widget__stat,
.dash-widget__metaand thestat/metacontext keys behind them are gone with
them.
- Owned by the profile view, now
-
The Monaco code editor is gone from the Wagtail admin. The HTML, CSS and JavaScript
fields on a block variant now use a fixed-height text area instead of an embedded
editor, which drops a bundled JavaScript loader and a runtime fetch from a
third-party CDN. No data or schema changes: the fields are unchanged and their
content is untouched. -
phoxtail install --urland--branchare gone;--sourcecovers both.
--url <repo>becomes--source git+<repo>, and--branch <ref>becomes an
@<ref>suffix on that locator —--source git+ssh://git@github.com/your-org/<package>.git@dev.
A baressh://orgit@host:remote is still understood without thegit+
prefix. -
The
.phoxtail-dialog--glassCSS class is gone. It provided the glass surface for
centered dialogs, but its only consumer became a right-to-left drawer and switched
to.phoxtail-drawer--glass, leaving nothing using it. Pages styling a centered
dialog with it will lose the background, border, and shadow; add those to the
consumer's own class, or reuse.phoxtail-drawer--glassif a drawer suits. -
The
appsfield is gone fromphoxtail.toml, andphoxtail hatchno longer
writes it. Nothing read it, and setting it never had any effect.
Fixed
phoxtail upgradenow works for any package in the project's dependency graph. It
matched a name inpyproject.tomlonly when a closing quote or an extras bracket
followed it, so every pinned dependency —wagtail>=7.4.2,<8.0and the like — was
refused as "not listed in pyproject.toml". Packages are now resolved through
uv.lock, which also covers transitive dependencies a project never declares
itself. When the lockfile does not move, the command says why and stops instead of
rebuilding and restarting the stack for an unchanged resolution.phoxtail installnow declares what it installs where you would expect to find it.
It sharedphoxtail upgrade's blind spot for pinned entries and would append a
second, unconstrained entry for a package already declared with a version
specifier. It also inserted new dependencies intodependency-groups.devrather
thanproject.dependencies, so anything installed into a hatched project was
dropped from the production image byuv sync --no-dev. When it cannot find a
dependencies array to append to, it now stops with an actionable message rather
than reporting a successful install of a package it never declared; a git source
installed into a freshly hatched project creates the[tool.uv.sources]table
instead of quietly resolving against PyPI, and where the table already exists the
source is inserted into it rather than appended to the end of the file, where it
could land in whichever table came last. A package declared only in the dev
dependency group no longer counts as installed, since the production image drops
that group.phoxtail install <package>no longer fails its pre-flight check for every package
on PyPI. It askeduv pip index versions, a subcommand no longer present as of uv
0.11, and read the resulting error as "not found on PyPI"; the check now queries
PyPI's JSON API directly. An unreachable index no longer blocks the install either
— a package is reported missing only when PyPI says it is.wagtailmedianow sits abovewagtail.snippetsinINSTALLED_APPS, so a project
starts under wagtailmedia 0.19.0. That release registers the media permission
policy in itsAppConfig.ready()while itswagtail_hooksmodule looks the policy
up at import time, andwagtail.snippets.ready()imports every app's hooks. With
snippets first, the lookup created a fallback policy and the real registration was
refused withImproperlyConfigured. Hatched projects carry the ordering fix in
their settings template; existing projects need the app moved by hand.- Admin menu entries that shared a
menu_orderno longer sort arbitrarily. Font
Roles and Palette Roles were both 150, and Wagtail 8 orders group members by
menu_orderrather than by their position in the group, so the tie surfaced as
Palette Roles jumping to the middle of the Design menu. The design viewsets are
renumbered to restore the intended order, and Remotes moves off 601 so it no longer
ties with API Tokens in the settings menu. - Modals opened from a page behind a language prefix no longer fail with "Invalid
content URL". The modal endpoint carries no prefix of its own, so it took the
language from the browser's cookie; it now reads the language from the content URL
it was asked to fetch, which also keeps the modal's own strings in the page's
language. phoxtail server provisionno longer reports a successful bootstrap when
cloud-init failed. It now asks cloud-init for its status instead of checking for a
file that is written either way, and the bootstrap ends on a Docker check so a
failed install is visible.phoxtail server provisionnow drops anyknown_hostsentry for a freshly created
server's IP before connecting. Providers recycle addresses, and a leftover key made
SSH refuse the connection outright — reported as a timeout, which sent you looking
at the wrong thing. A failed SSH wait now quotes what SSH actually said.phoxtail server provisionnow installs Docker on images that shipgrub-pcwith
a stored install device that doesn't exist. The failed bootloader upgrade left
dpkghalf-configured, and a half-configureddpkgfails every laterapt-get,
Docker's included. The bootstrap now declines that install rather than attempting
it, leaving the bootloader as the image shipped it, and acts only when the stored
device is invalid — so healthy images are untouched.phoxtail server deploynow checks that the server can actually read the image
from the registry, rather than assuming a login exists becauseghcr.ioappears in
~/.docker/config.json. An expired token, or one with no access to the package,
previously reported success and failed later at pull time withdenied.- Building phoxtail from source now requires setuptools 77.0.1 or newer, which the
declared floor previously understated. Nothing changes for anyone installing a
wheel.