MishkaCmsCore. Runtime. Compilers. TailwindCompiler
(Mishka CMS Core v0.0.5-alpha.5)
Copy Markdown
View Source
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
The host shell in a site's one Tailwind run
A site's stylesheet is one Tailwind run over the host's runtime shell (the code that renders
around a database page, see host_app_web_sources/0), app.css, js_assets and the site's
Component, Page, Layout and Stylesheet rows. Tailwind ranks every utility once, and a shell
class is built from the same @theme tokens as the site's own, a DB :theme row's included.
In development the run scans the shell's .ex files. A release ships none, so there it reads
priv/static/temp/shell_classes.txt instead: the classes Tailwind's own scanner found in those
files when the release was built (mix compile_static_css in the host).
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:
# The host shell; a release reads priv/static/temp/shell_classes.txt in its place
@source "lib/mishka_cms_web/components/**/*.{ex,heex,eex}"
@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, :success, "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 site's stylesheet, built as compile/1
builds it in one Tailwind run, with the draft's OWN template scanned too (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).
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.
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.
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, wherever they are written — at the top, inside @media, @layer or a rule, or inside a
@font-face or a keyframe, where CSS has no at-rule but Tailwind still follows one:
@import, unless it names anhttp://orhttps://address (a font stylesheet), which a build then puts ahead of everything else, where CSS obeys it;@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, and so is an @ in a comment, a string or a url(). The at-rules are found
and removed by igniter_css's parser (IgniterCss.list_at_rules/2,
IgniterCss.remove_at_rules/3). Text it refuses to edit, such as braces that do not balance, is
left out whole when it holds one of those at-rules: {"", [reason]}.
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.
ONE Tailwind run over everything the site's pages can render: the host shell, app.css,
js_assets, the globally-installed JS hooks and stylesheet rows, the global UI-kit theme rows,
and the site's own reachable templates and rows (build_css_content_site/2).
It used to be two runs joined end to end — the cached shared base, then a run over the site's own
templates. Tailwind ranks utilities only within a run, so a utility both runs emitted came again
after every responsive variant of the first: the host shell's px-4 sm:px-6 lg:px-8 stayed at
px-4 on every screen once a page of the site used px-4 too. And the global theme rows were
pasted into both runs, so each of their plain rules, @font-face and @import was in the
stylesheet twice. One run ranks every utility once and holds every row once.
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
A global writer rebuilds the shared base in place (compile_and_store_shared_base/0) and then
every site; the dev watcher drops the base with delete_shared_base_css/0.
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
@spec compile_preview(String.t(), String.t(), String.t(), String.t(), keyword()) :: {:ok, String.t()} | {:error, String.t()}
Compile a ONE-OFF CSS bundle for a draft/preview: the site's stylesheet, built as compile/1
builds it in one Tailwind run, with the draft's OWN template scanned too (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}.
opts takes rows:, %{name => fun}: the site's stylesheet row called name is compiled as
fun.(content) instead of its saved content, and a name the site has no row for is added after its
rows as fun.(nil). Both pass through Tailwind like a saved row, so @apply and nesting resolve.
This is how a draft's own element CSS reaches its preview — see
MishkaCmsCore.Runtime.DraftPreviewAssets.css/2.
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.
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, :success, "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.