Builds the two BUILD-TIME stylesheets, into the host app's priv/static.
One command, no prerequisites for whoever installs this CMS:
- compiles
admin.css— every Tailwind utility the dashboard, the page builder and the login page use, scanned out of the host app and every extension; - compiles
public.css— the same for the host's own marketing pages, from the host's web tree alone; - installs the vendored Tailwind browser build, which the page builder's canvas loads;
- installs the admin's web fonts.
Both sheets exist because their classes live in .ex files and cannot change without a deploy.
The third audience — a SITE — has classes in database rows that change without one, which is why
MishkaCmsCore.Runtime.Compilers.TailwindCompiler builds those at runtime. Three audiences, three
sheets, no overlap; they used to be a single 2.4 MB file that every one of them downloaded.
All three ship inside this library's priv/admin_assets/, so a host gets them by depending on it.
Nothing is fetched at build time and nothing is expected to be there already.
source(none) is load-bearing
Without it Tailwind v4 auto-detects sources from the CSS file's own tree in ADDITION to the
@source lines below, and walks into priv/cms_ui_kits/chelekom.json — an 11.7 MB kit bundle whose
every component template is a string inside JSON. Tailwind cannot tell that from markup, so it
compiled a rule for every class of all 244 components plus 1,288 heroicons: 2,511,005 bytes and an
11-second build, for classes the admin chrome never renders. Those components appear only inside
the editor's canvas, which loads Tailwind Live and generates them on demand.
With source(none), only the explicit @source lines below are scanned. 589,694 bytes, 836 ms.
Why a task and not a config :tailwind profile
Because the @source globs have to be GENERATED. Tailwind v4 skips gitignored entries while
WALKING a directory, and a CMS install routinely gitignores vendored path dependencies and always
gitignores deps/ — so @source "__extensions/**" silently matches nothing for them. Absolute
paths do not help: the filter applies to the walk, not to the form of the path. Naming each
directory outright is what bypasses it, and that list can only be built by listing the disk.
That is not a hypothetical. Compiling this without the per-directory globs produced a dashboard
whose tables had lost their responsive layout, because one Gervaz line
(max-[980px]:overflow-visible! and friends) lived in a gitignored extension.
Why the admin is compiled at all, when a site is not
A site's classes live in database rows that change without a deploy, which is why
MishkaCmsCore.Runtime.Compilers.TailwindCompiler builds site CSS at runtime. The admin's classes
live in templates and are fixed when the release is built, so compiling them per request bought
nothing — it cost a 275 KB browser JIT and an in-browser compile on every admin page.
The page builder still loads that JIT, and always will: a class typed into the inspector, or a kit component whose classes no build ever scanned, has to exist a frame later.
Why neither sheet may go into the host's app.css
TailwindCompiler.build_css_content_static_base/0 inlines that file wholesale into the base every
site's CSS is built on. Compiling the dashboard into it would ship the entire admin inside every
public page's stylesheet — which is the bug this whole split exists to undo.
Usage
mix admin_css # once, as part of assets.build / assets.deployIn development the endpoint watches each audience in its own process — see watch/1. --watch is
refused here, because Tailwind's watch never returns and only the first of the two sheets would
ever be built.
Summary
Functions
Rebuilds ONE audience on every change, for the endpoint's :watchers list.
Functions
@spec watch(keyword()) :: non_neg_integer()
Rebuilds ONE audience on every change, for the endpoint's :watchers list.
The admin used to be styled by the browser JIT, which generated a class the moment it appeared in the DOM — so an edit to an admin template needed no build step at all. Serving compiled CSS means something has to notice that edit, and without this a developer adds a class, sees nothing happen, and has no reason to suspect the stylesheet.
One audience per watcher, and why it cannot be both
Tailwind's --watch does not return. A single call that built the admin sheet and then the public
one therefore never reached the second: admin.css rebuilt on every keystroke while public.css
sat untouched for days, so an edit to a marketing page produced no rule and the class silently did
nothing — the exact failure described above, fixed for the admin and left live for the public half.
Each audience gets its own entry, which Phoenix runs in its own process:
watchers: [
admin_css: {Mix.Tasks.AdminCss, :watch, [[only: :admin]]},
public_css: {Mix.Tasks.AdminCss, :watch, [[only: :public]]}
]