Production-ready Tailwind CSS v4 compiler for runtime CSS generation with multi-site support.
The TailwindCompiler provides comprehensive CSS compilation capabilities for the MishkaCms
ecosystem, integrating with AssetsState for efficient site-specific CSS management and
caching. It supports dynamic template scanning, custom variants, Phoenix LiveView integration,
and production-optimized asset compression.
Features
- Multi-Site CSS Compilation: Generate and manage CSS for multiple sites independently
- Runtime Template Scanning: Dynamically scans templates and JavaScript assets for Tailwind classes
- Phoenix LiveView Integration: Built-in support for LiveView loading states and lifecycle
- Asset Compression: Automatic Brotli and Gzip compression for optimized delivery
- AssetsState Integration: Seamless integration with MishkaCms asset management system
- Production Optimization: Minified output with configurable source paths
- Dark Mode Support: Custom dark mode variant using data attributes
- Custom Variants: Pre-configured Phoenix-specific loading state variants
Static base (compile time) vs runtime DB content
Tailwind classes reach this CMS from two timelines:
Compile time — the host app's
lib/**, the extensions (__extensions/**, laterdeps/mishka_*), andassets/css/app.css. These don't change after a release is built, so Tailwind's own scanner reads them ONCE viamix mishka.compile_static_css, which writespriv/static/temp/static_base.css. That file ships in the release and is prepended verbatim to every site's CSS. No regex — the scanner reads everyclass={[...]}list,&&conditional, macro and sigil shape natively.Runtime — DB-stored
Component/Page/Layout/Stylesheetrows. These change when an admin saves, so they're scanned by Tailwind at boot and on each per-resource Oban job, then concatenated on top of the static base.
In development the static_base.css file is absent, so the compiler
scans the live source tree directly (see host_app_web_sources/0).
Tailwind has the real files, so nothing is missed. The precompiled file
exists only for releases, where the source tree isn't on disk.
Theme tokens (@theme) — the one trade-off to know
A custom utility such as bg-brand-500 only generates CSS if its token
(--color-brand-500) is defined in an @theme {} block present in the
SAME Tailwind compile. Tokens can live in two places:
assets/css/app.css(or a kit theme file) — present at compile time.- A
StylesheetDB row ofkind: :theme— present only at runtime.
This produces one rule you must respect:
| Where the class is used | Where its token lives | Result |
|---|---|---|
| DB content (component/page) | DB :theme row | ✅ both meet at runtime |
| DB content | app.css | ✅ token present at runtime too |
Static code (lib/, ext) | app.css | ✅ token present at compile time |
| Static code | DB :theme only | ❌ dropped — see below |
The failure case: a custom color used in static code whose token lives only in the DB. At compile time the token doesn't exist, so the static base never gets the rule; at runtime the class isn't in DB content, so it's never generated. It silently produces no CSS.
Rule: custom color in a static file ⇒ define its token in a static file
To use bg-brand-500 in a LiveView/component under lib/ or an extension,
declare the token in assets/css/app.css:
/* assets/css/app.css */
@theme {
--color-brand-500: oklch(0.62 0.21 12);
}Now the token is present at compile time, static_base.css resolves
bg-brand-500, and it ships in the release. A per-site DB theme can still
override the visual value at runtime through the :custom/:theme
stylesheet layers — it just can't introduce a brand-new token that static
code depends on.
Basic Usage
# Compile CSS for a specific site
case TailwindCompiler.compile("my_site") do
{:ok, css} ->
IO.puts("CSS compiled successfully")
IO.puts("Size: #{byte_size(css)} bytes")
{:error, reason} ->
IO.puts("Compilation failed: #{reason}")
endIntegrated Workflow with AssetsState
# Compile and automatically store in AssetsState with compression
case TailwindCompiler.compile_and_store("production_site") do
{:ok, css} ->
# CSS is now available in multiple formats:
# - Raw CSS
# - Brotli compressed
# - Gzip compressed
IO.puts("CSS compiled and cached successfully")
{:error, reason} ->
Logger.error("Failed to compile CSS: #{reason}")
end
# Retrieve compiled CSS from cache
css = TailwindCompiler.get_compiled_css("production_site")
# Get compressed version for optimal delivery
brotli_css = TailwindCompiler.get_compiled_css("production_site", "brotli_css")
gzip_css = TailwindCompiler.get_compiled_css("production_site", "gzip_css")Asset Management Operations
# Check compilation status
status = TailwindCompiler.get_css_status("my_site")
# Returns: %{
# site: "my_site",
# compiled: true,
# updated_at: ~U[2024-01-01 12:00:00Z],
# css_size: 125_000
# }
# Refresh CSS (recompile and update cache)
{:ok, fresh_css} = TailwindCompiler.refresh_css("my_site")
# Check if CSS exists
if TailwindCompiler.css_exists?("my_site") do
IO.puts("CSS is available")
end
# Clean up CSS for a site
:ok = TailwindCompiler.delete_compiled_css("my_site")CSS Source Configuration
The compiler automatically configures Tailwind CSS v4 with comprehensive source scanning:
# static_base.css (compile-time) prepended verbatim — host app + extensions
@source "runtime/my_site/js_hook/**/*.template" # Reachable by no walk, so globbed
@source "runtime/my_site/stylesheet/**/*.template"
@source "runtime/__global__/js_hook/**/*.template"
@source "runtime/__global__/stylesheet/**/*.template"
@source "runtime/my_site/page/home.template" # One line per REACHABLE template:
@source "runtime/my_site/layout/main.template" # the site's pages and error pages, the
@source "runtime/__global__/component/alert.template" # layouts they use, the components
@source "runtime/__global__/component/icon.template" # they name — see `closure_roots/2`
@source "js_assets/js/**/*.js" # Runtime JS hooks (NOT vendor/ — see below)
@source "js_assets/js/**/*.ts" # TypeScript filesCustom Variants and Phoenix Integration
/* Phoenix LiveView loading states */
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
/* Data attribute-based dark mode */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));Usage in Phoenix Controllers/LiveViews
defmodule MyAppWeb.PageController do
use MyAppWeb, :controller
def show(conn, %{"site" => site_name}) do
# Ensure CSS is compiled for the site
case TailwindCompiler.compile_and_store(site_name) do
{:ok, _css} ->
render(conn, :show, site: site_name)
{:error, reason} ->
conn
|> put_flash(:error, "Failed to load site styles")
|> redirect(to: "/")
end
end
end
defmodule MyAppWeb.AdminLive do
use MyAppWeb, :live_view
def handle_event("refresh_css", %{"site" => site}, socket) do
case TailwindCompiler.refresh_css(site) do
{:ok, _css} ->
{:noreply, put_flash(socket, :info, "CSS refreshed successfully")}
{:error, reason} ->
{:noreply, put_flash(socket, :error, "Failed to refresh CSS: #{reason}")}
end
end
endPerformance Optimization
# Batch operations for multiple sites
sites = ["site1", "site2", "site3"]
compiled_sites =
Task.async_stream(sites, &TailwindCompiler.compile_and_store/1,
max_concurrency: 3,
timeout: 30_000
)
|> Enum.map(fn {:ok, result} -> result end)Error Handling and Debugging
case TailwindCompiler.compile("problematic_site") do
{:ok, css} ->
Logger.info("Successfully compiled #{byte_size(css)} bytes of CSS")
{:error, reason} ->
Logger.error("Tailwind compilation failed: #{reason}")
# Check system requirements
if !System.find_executable("node") do
Logger.error("Node.js not found - required for Tailwind compilation")
end
# Verify template paths exist
temp_dir = System.tmp_dir()
Logger.info("Using temporary directory: #{temp_dir}")
endProduction Deployment Considerations
- Asset Precompilation: Consider pre-compiling CSS during deployment for faster startup
- Memory Management: CSS assets are cached in memory; monitor memory usage with many sites
- Concurrent Compilation: Use Task.async_stream for bulk compilation operations
- File System Access: Ensure proper permissions for temporary directory operations
- Node.js Dependency: Requires Node.js runtime for Tailwind CLI execution
Asset Compression Details
All compiled CSS is automatically compressed using multiple algorithms:
- Brotli: Best compression ratio, supported by modern browsers
- Gzip: Universal compatibility, good compression ratio
- Raw CSS: Uncompressed for development and fallback scenarios
The compiler calculates MD5 hashes for cache invalidation and stores metadata including compilation timestamps and asset sizes.
Summary
Functions
The CSS an author wrote, without the at-rules that make the Tailwind binary read a file or run
code on the server, and the names of the ones left out: {css, ["@import", …]}.
The templates a visitor can reach on site_name — the roots of its CSS closure.
Compiles Tailwind CSS from dynamic templates and assets for a specific site.
Compiles Tailwind CSS and stores all compressed variants in AssetsState for optimized delivery.
Compiles shared base CSS and stores it in AssetsState for reuse.
Compile a ONE-OFF CSS bundle for a draft/preview: the shared base + the site's classes + the
draft's OWN template (so brand-new classes in an unpublished draft are styled). Writes the draft
template into an isolated temp namespace, scans it alongside the site, and removes it after — it
NEVER touches the live site bundle. Returns {:ok, css} | {:error, reason}.
Compiles ONLY the CSS every site shares: the host shell, app.css, js_assets, and the global
templates no reachability walk can reach (JS hooks and stylesheet rows).
Compiles the static base CSS at compile/release time and writes it to
static_base_path/0.
The classes the stored content of the site site_name writes — a class="bg-[#282828]" in its
HTML, a marker's class and *_class attributes, and the attributes its component takes a class
string in (MishkaCmsCore.Runtime.ContentComponents.site_classes/2). No template holds them, so
no scan finds them: the site's stylesheet is built with each as an @source inline(…) candidate.
The component names the stored content of the site site_name uses — what a template drawing
that content reaches (MishkaCmsCore.Runtime.Compilers.CssClosure).
The icon classes the stored content of the site site_name names — a marker's
icon="hero-inbox", a <span class="hero-inbox"> in its HTML
(MishkaCmsCore.Runtime.ContentComponents.icons/1). No template holds them, so no scan finds
them: the site's stylesheet carries a rule for each a registered set ships, beside the icons its
templates name.
Checks if compiled CSS exists for a specific site in the cache.
Deletes all compiled CSS variants for a specific site from the cache.
Invalidate the cached shared-base CSS so the next compile rebuilds it from scratch.
Ensures Tailwind binary is installed. Call once at boot time.
Retrieves compiled CSS from AssetsState cache for a specific site and format.
Gets comprehensive CSS compilation status and metadata for a specific site.
Gets the cached shared base CSS, or compiles it if not available.
Refreshes CSS for a site by recompiling and updating the cache.
Reads the version a tailwindcss --help output names, whether or not the binary painted it.
Enqueues the stylesheet build of the site site_id names — its "css" job, on the per-site path
that coalesces a burst of writes into one build. For a write that changed what the stylesheet is
built from without a component, page or layout being written: content that names a component, an
icon or a class it did not name before.
Where the compiled runtime templates and CSS live.
Whether a candidate stylesheet fragment compiles for a site, in Tailwind's own words when it does
not: :ok | {:error, message}.
Functions
The CSS an author wrote, without the at-rules that make the Tailwind binary read a file or run
code on the server, and the names of the ones left out: {css, ["@import", …]}.
A stylesheet row is pasted into the file Tailwind reads. There @import "../x.css" copies any
local CSS file into the page's public CSS, @source "../x" scans any local file for class names,
and @plugin, @config and @reference load a local file — a plugin is JavaScript Tailwind runs.
Left out:
@import, unless it names anhttp://orhttps://address (a font stylesheet);@source, unless it is@source inline(…), which only lists class names;@plugin,@configand@reference;- any at-rule whose name is written with a backslash escape.
Everything else — @theme, @apply, @media, @layer, @custom-variant, @utility — is kept
as it was written.
The templates a visitor can reach on site_name — the roots of its CSS closure.
A site's pages and error pages are roots because a visitor can ask for either by URL. Its LAYOUTS
are not: a layout is chosen by a database relation rather than named in markup, so a site with
thirty layout templates and five pages was scanning twenty-five it can never render, and every
global layout sat in every site's stylesheet. The layouts this site's pages actually name are read
from pages.layout_id and added here — which is also what makes editing a global layout a rebuild
of the sites that use it rather than a fact about all of them.
Read here rather than inside MishkaCmsCore.Runtime.Compilers.CssClosure, which walks a directory
and deliberately knows nothing about the database. If the read fails the layouts fall back to the
whole directory, on the same principle as :unbounded — correct-and-large, never small-and-broken.
Compiles Tailwind CSS from dynamic templates and assets for a specific site.
This optimized version:
- Uses cached shared base CSS (global templates, js_assets) - avoids rescanning N times
- Only compiles site-specific templates
- Combines shared base + site-specific CSS
Parameters
site_name- String identifier for the site (used for template scanning and caching)
Return Values
{:ok, css}- Successfully compiled CSS as a minified string{:error, reason}- Compilation failed
Performance Optimization
With 20 sites, instead of scanning global templates 20 times:
- Shared base compiled ONCE (~225ms)
- Each site only scans its own templates (~50-100ms)
- Total: ~225ms + 20×75ms = ~1.7s instead of 20×225ms = ~4.5s
There used to be a second way to answer this — compile_full/1, a use_shared_base: false mode
that assembled the whole stylesheet in one Tailwind run. Nothing called it outside its own tests,
and it restated every decision made here, so the two drifted apart with each change to what gets
scanned. A stale shared base is dropped with delete_shared_base_css/0 instead, which is what
every writer already does.
Compiles Tailwind CSS and stores all compressed variants in AssetsState for optimized delivery.
This function combines compilation with intelligent caching by automatically generating and storing multiple compressed formats (raw, Brotli, Gzip) along with metadata for efficient asset serving. It provides the complete workflow for production CSS management.
Parameters
site_name- String identifier for the site
Return Values
{:ok, css}- Successfully compiled and stored CSS (returns the raw CSS string){:error, reason}- Compilation or storage failed with error message
Storage Process
- CSS Compilation: Generates minified CSS using
compile/1 - Multi-format Compression: Creates Brotli and Gzip compressed variants
- Metadata Generation: Calculates MD5 hash and timestamp for cache invalidation
- Batch Storage: Atomically stores all formats in AssetsState
- Logging: Records successful operations for monitoring
Stored Asset Formats
"css"- Raw minified CSS for development and fallback"brotli_css"- Brotli compressed CSS (best compression, ~20-30% smaller)"gzip_css"- Gzip compressed CSS (universal compatibility, ~15-25% smaller)
Examples
# Basic compilation and storage
case TailwindCompiler.compile_and_store("ecommerce_site") do
{:ok, css} ->
IO.puts("CSS compiled and cached")
IO.puts("Available formats: css, brotli_css, gzip_css")
{:error, reason} ->
Logger.error("Failed to compile ecommerce CSS: #{reason}")
end
# Use in a Phoenix plug for automatic CSS generation
defmodule EnsureCssPlug do
def init(opts), do: opts
def call(conn, _opts) do
site = get_site_from_conn(conn)
if !TailwindCompiler.css_exists?(site) do
case TailwindCompiler.compile_and_store(site) do
{:ok, _css} -> conn
{:error, reason} ->
conn
|> put_status(500)
|> Phoenix.Controller.text("CSS compilation failed: #{reason}")
|> halt()
end
else
conn
end
end
end
# Bulk compilation for multiple sites
sites = ["blog", "shop", "portfolio"]
results =
Task.async_stream(
sites,
&TailwindCompiler.compile_and_store/1,
max_concurrency: 2,
timeout: 60_000
)
|> Enum.map(fn {:ok, result} -> result end)Asset Metadata
Each stored asset includes comprehensive metadata:
hash- MD5 hash for cache busting and integrity verificationupdated_at- UTC timestamp for cache managementvalue- The actual CSS content (raw or compressed)type- Format identifier ("css", "brotli_css", "gzip_css")
Integration with HTTP Serving
defmodule CssController do
def serve_css(conn, %{"site" => site}) do
content_encoding = get_best_encoding(conn)
css_type = encoding_to_type(content_encoding)
case AssetsState.get(site, css_type) do
{:ok, asset} ->
conn
|> put_resp_content_type("text/css")
|> put_resp_header("content-encoding", content_encoding)
|> put_resp_header("etag", asset.hash)
|> put_resp_header("cache-control", "public, max-age=31536000")
|> send_resp(200, asset.value)
{:error, :not_found} ->
# Trigger compilation on demand
case TailwindCompiler.compile_and_store(site) do
{:ok, _css} -> serve_css(conn, %{"site" => site})
{:error, _} -> send_resp(conn, 500, "CSS unavailable")
end
end
end
endPerformance Benefits
- Reduced Bandwidth: Compression reduces transfer sizes by 15-30%
- Faster Loading: Pre-compressed assets eliminate server-side compression overhead
- Cache Efficiency: Hash-based versioning enables aggressive browser caching
- CDN Optimization: Multiple formats support optimal CDN distribution
Compile a ONE-OFF CSS bundle for a draft/preview: the shared base + the site's classes + the
draft's OWN template (so brand-new classes in an unpublished draft are styled). Writes the draft
template into an isolated temp namespace, scans it alongside the site, and removes it after — it
NEVER touches the live site bundle. Returns {:ok, css} | {:error, reason}.
Compiles the static base CSS at compile/release time and writes it to
static_base_path/0.
Scans the RUNTIME SHELL — the layouts module, its templates and RuntimeLive — plus the JS assets
and app.css (for @theme tokens), with Tailwind's own scanner. The result ships in the release
and is prepended to every site's CSS at runtime, because a release has no .ex files to scan.
Run from mix mishka.compile_static_css (wired into the release build).
See the moduledoc "Static base" and "Theme tokens" sections.
The classes the stored content of the site site_name writes — a class="bg-[#282828]" in its
HTML, a marker's class and *_class attributes, and the attributes its component takes a class
string in (MishkaCmsCore.Runtime.ContentComponents.site_classes/2). No template holds them, so
no scan finds them: the site's stylesheet is built with each as an @source inline(…) candidate.
Answered in classes by the same runtime:compile:content_components event as
content_components/1; a token that could not be a class is left out, and at most 20000 are
kept. [] for a site SiteState does not know, and while the event cannot run.
The component names the stored content of the site site_name uses — what a template drawing
that content reaches (MishkaCmsCore.Runtime.Compilers.CssClosure).
Asked of whoever stores content through runtime:compile:content_components, with
%{site: name, site_id: id, components: [], icons: [], classes: []}: a plugin adds the names its
content uses to components, the icon classes it names to icons (content_icons/1), and the
classes it writes to classes (content_classes/1). Read while a stylesheet is built, never in a
request. [] for a site SiteState does not know, and while the event cannot run.
def call(%{site_id: site_id, components: names, icons: icons, classes: classes} = data) do
{:reply,
%{
data
| components: names ++ names_my_posts_use(site_id),
icons: icons ++ icons_my_posts_use(site_id),
classes: classes ++ classes_my_posts_use(site_id)
}}
end
The icon classes the stored content of the site site_name names — a marker's
icon="hero-inbox", a <span class="hero-inbox"> in its HTML
(MishkaCmsCore.Runtime.ContentComponents.icons/1). No template holds them, so no scan finds
them: the site's stylesheet carries a rule for each a registered set ships, beside the icons its
templates name.
Answered in icons by the same runtime:compile:content_components event as
content_components/1. [] for a site SiteState does not know, and while the event cannot run.
Checks if compiled CSS exists for a specific site in the cache.
This function provides a quick boolean check for CSS availability without retrieving the actual content, making it ideal for conditional compilation logic and cache validation.
Parameters
site_name- String identifier for the site
Return Value
true- CSS exists and is available in the cachefalse- CSS has not been compiled or has been removed
Examples
# Check before attempting to serve CSS
if TailwindCompiler.css_exists?("blog_site") do
css = TailwindCompiler.get_compiled_css("blog_site")
send_css_response(conn, css)
else
# Trigger compilation
{:ok, _css} = TailwindCompiler.compile_and_store("blog_site")
redirect_to_css(conn)
end
# Batch check multiple sites
sites = ["site1", "site2", "site3"]
compiled_sites = Enum.filter(sites, &TailwindCompiler.css_exists?/1)
missing_sites = sites -- compiled_sites
# Use in a deployment health check
defmodule HealthCheck do
def css_status do
required_sites = Application.get_env(:app, :required_sites)
Enum.map(required_sites, fn site ->
%{
site: site,
css_ready: TailwindCompiler.css_exists?(site)
}
end)
end
end
Deletes all compiled CSS variants for a specific site from the cache.
This function removes all cached CSS formats (raw, Brotli, Gzip) for a site, effectively clearing the cache and forcing recompilation on the next request. Useful for cache invalidation when templates or styles are updated.
Parameters
site_name- String identifier for the site
Return Value
:ok- CSS successfully deleted or didn't exist
Examples
# Delete CSS after template updates
:ok = TailwindCompiler.delete_compiled_css("updated_site")
# Next request will trigger recompilation
{:ok, fresh_css} = TailwindCompiler.compile_and_store("updated_site")
# Use in a cache management task
defmodule CacheManager do
def invalidate_site_css(site_name) do
Logger.info("Invalidating CSS cache for site: #{site_name}")
:ok = TailwindCompiler.delete_compiled_css(site_name)
# Optionally precompile immediately
Task.start(fn ->
TailwindCompiler.compile_and_store(site_name)
end)
end
end
# Bulk cache clearing
sites_to_clear = ["site1", "site2", "site3"]
Enum.each(sites_to_clear, &TailwindCompiler.delete_compiled_css/1)Use Cases
- Template Updates: Clear cache when site templates are modified
- Development: Reset styles during development iterations
- Deployment: Clear cache as part of deployment pipeline
- Memory Management: Periodically clear unused site CSS
- Error Recovery: Clear potentially corrupted cached CSS
Ensures Tailwind binary is installed. Call once at boot time.
This avoids repeated install checks during install_and_run/2 calls.
After calling this, use run/2 instead of install_and_run/2.
Retrieves compiled CSS from AssetsState cache for a specific site and format.
This function provides fast access to pre-compiled CSS assets without triggering recompilation. It supports multiple compression formats for optimal delivery based on client capabilities and performance requirements.
Parameters
site_name- String identifier for the sitetype- Asset format type (defaults to"css")"css"- Raw minified CSS"brotli_css"- Brotli compressed CSS"gzip_css"- Gzip compressed CSS
Return Value
binary()- The CSS content as a binary string- Returns a CSS comment with error message if the asset doesn't exist
Examples
# Get standard CSS
css = TailwindCompiler.get_compiled_css("my_site")
# Get compressed format for optimal delivery
brotli_css = TailwindCompiler.get_compiled_css("my_site", "brotli_css")
gzip_css = TailwindCompiler.get_compiled_css("my_site", "gzip_css")
# Use in Phoenix controller with content negotiation
defmodule AssetController do
def css(conn, %{"site" => site}) do
format = case get_req_header(conn, "accept-encoding") do
[encoding] when is_binary(encoding) ->
cond do
String.contains?(encoding, "br") -> "brotli_css"
String.contains?(encoding, "gzip") -> "gzip_css"
true -> "css"
end
_ -> "css"
end
css_content = TailwindCompiler.get_compiled_css(site, format)
conn
|> put_resp_content_type("text/css")
|> maybe_put_encoding_header(format)
|> send_resp(200, css_content)
end
defp maybe_put_encoding_header(conn, "brotli_css"),
do: put_resp_header(conn, "content-encoding", "br")
defp maybe_put_encoding_header(conn, "gzip_css"),
do: put_resp_header(conn, "content-encoding", "gzip")
defp maybe_put_encoding_header(conn, _), do: conn
endError Handling
# CSS not found scenario
css = TailwindCompiler.get_compiled_css("non_existent_site")
# Returns: "/* CSS not found for site: non_existent_site */"
# Handle missing CSS gracefully
css = case TailwindCompiler.get_compiled_css("my_site") do
"/* CSS not found" <> _ ->
# Trigger compilation if CSS is missing
case TailwindCompiler.compile_and_store("my_site") do
{:ok, compiled_css} -> compiled_css
{:error, _} -> "/* Compilation failed */"
end
valid_css -> valid_css
endPerformance Notes
- This function performs a direct cache lookup with minimal overhead
- No disk I/O or compilation occurs - purely memory-based retrieval
- Consider using compressed formats for production traffic
- Falls back gracefully with informative error messages for missing assets
Gets comprehensive CSS compilation status and metadata for a specific site.
This function returns detailed information about the CSS compilation state, including availability, timestamps, and size metrics. It's useful for monitoring, debugging, and administrative interfaces.
Parameters
site_name- String identifier for the site
Return Value
Returns a map with the following keys:
site- The site name (string)compiled- Boolean indicating if CSS existsupdated_at- UTC DateTime of last compilation (ornilif not compiled)css_size- Size of CSS in bytes (or0if not compiled)
Examples
# Check single site status
status = TailwindCompiler.get_css_status("my_site")
# => %{
# site: "my_site",
# compiled: true,
# updated_at: ~U[2024-01-15 10:30:45Z],
# css_size: 125_430
# }
# Use in admin dashboard
defmodule AdminDashboard do
def site_status_list do
Application.get_env(:app, :managed_sites)
|> Enum.map(&TailwindCompiler.get_css_status/1)
|> Enum.sort_by(& &1.updated_at, {:desc, DateTime})
end
end
# Monitor compilation staleness
status = TailwindCompiler.get_css_status("production_site")
if status.compiled do
age_hours = DateTime.diff(DateTime.utc_now(), status.updated_at, :hour)
if age_hours > 24 do
Logger.warning("CSS for #{status.site} is #{age_hours} hours old")
TailwindCompiler.refresh_css(status.site)
end
else
Logger.error("No CSS compiled for critical site: #{status.site}")
TailwindCompiler.compile_and_store(status.site)
end
# Health check endpoint
defmodule HealthController do
def css_health(conn, _params) do
critical_sites = ["main", "blog", "shop"]
statuses = Enum.map(critical_sites, &TailwindCompiler.get_css_status/1)
all_compiled = Enum.all?(statuses, & &1.compiled)
total_size = Enum.sum(Enum.map(statuses, & &1.css_size))
health_data = %{
status: if(all_compiled, do: "healthy", else: "degraded"),
sites: statuses,
total_css_size: total_size,
checked_at: DateTime.utc_now()
}
json(conn, health_data)
end
endMonitoring Use Cases
- Admin Dashboards: Display CSS compilation status across all sites
- Health Checks: Verify critical site CSS is available
- Performance Monitoring: Track CSS size and compilation frequency
- Alerting: Trigger alerts for missing or stale CSS
- Analytics: Gather metrics on CSS usage patterns
Refreshes CSS for a site by recompiling and updating the cache.
This function forces a complete recompilation of CSS for a site, regardless of whether cached CSS already exists. It combines cache invalidation with fresh compilation, ensuring the most up-to-date styles are available.
Parameters
site_name- String identifier for the site
Return Values
{:ok, css}- Successfully recompiled and cached fresh CSS{:error, reason}- Compilation failed with error message
Examples
# Refresh CSS after template changes
case TailwindCompiler.refresh_css("blog_site") do
{:ok, css} ->
Logger.info("Blog CSS refreshed: #{byte_size(css)} bytes")
{:error, reason} ->
Logger.error("Failed to refresh blog CSS: #{reason}")
end
# Use in a Phoenix LiveView for admin functionality
defmodule AdminLive do
def handle_event("refresh_css", %{"site" => site}, socket) do
case TailwindCompiler.refresh_css(site) do
{:ok, _css} ->
{:noreply, put_flash(socket, :info, "CSS refreshed for #{site}")}
{:error, reason} ->
{:noreply, put_flash(socket, :error, "Refresh failed: #{reason}")}
end
end
end
# Scheduled CSS refresh for active sites
defmodule CssRefreshWorker do
use Oban.Worker, queue: :css_refresh
@impl Oban.Worker
def perform(%Oban.Job{args: %{"site" => site}}) do
case TailwindCompiler.refresh_css(site) do
{:ok, _css} -> :ok
{:error, reason} -> {:error, reason}
end
end
end
# Bulk refresh for multiple sites
sites = ["blog", "shop", "docs"]
results =
Task.async_stream(sites, &TailwindCompiler.refresh_css/1)
|> Enum.map(fn {:ok, result} -> result end)When to Use
- Template Updates: After modifying site templates or components
- Style Changes: When custom CSS or Tailwind configuration changes
- Deployment: As part of the deployment process to ensure fresh styles
- Development: During active development for immediate style updates
- Maintenance: Periodic refresh to clear any potential cache inconsistencies
Reads the version a tailwindcss --help output names, whether or not the binary painted it.
Pass the raw output. Returns the number as text ("4.3.2"), or nil when the output names no
version.
@spec schedule(String.t()) :: :ok | {:ok, Oban.Job.t()} | {:error, term()}
Enqueues the stylesheet build of the site site_id names — its "css" job, on the per-site path
that coalesces a burst of writes into one build. For a write that changed what the stylesheet is
built from without a component, page or layout being written: content that names a component, an
icon or a class it did not name before.
A switched-off or unknown site has nothing to build, and is :ok. So is every call under the
:env_test? flag, as for every other runtime job.
@spec temp_dir() :: String.t()
Where the compiled runtime templates and CSS live.
Public because MishkaCmsCore.Runtime.Compilers.CssClosure walks the same tree.
Whether a candidate stylesheet fragment compiles for a site, in Tailwind's own words when it does
not: :ok | {:error, message}.
For CSS that is ABOUT TO BE STORED as a row. Tailwind v4 answers @apply of a utility it does not
know with a non-zero exit, and a stored fragment that does that fails every later compile in its
scope — so a writer proves it before writing it rather than after.
Nothing is scanned: the fragment is compiled against the site's theme rows, tailwind_preamble/0
and the utility definitions alone, with no @source line, which is what makes this affordable on
a save. The preamble is the half that is easy to forget and the half that matters — a check
missing the plugin and the variants is STRICTER than the compiles it stands in for, and refuses
utilities this CMS ships. It also means this answers for the fragment and nothing else: a row
that is already broken cannot fail somebody's valid class.