Features

Everything PhoenixPrerender provides

prerendered

This page was generated at build time and is served as static HTML.

Build-Time Static Generation

Routes marked with prerender do are discovered automatically. The generator renders each through the complete Phoenix endpoint pipeline using Phoenix.ConnTest.dispatch/5, ensuring all plugs, layouts, and components execute.

Pages are rendered concurrently via Task.async_stream with configurable concurrency. Default parallelism matches System.schedulers_online().

Per-Route Prerender Modes

Dead views inside prerender do are prerendered by default and served to everyone. LiveView routes require explicit metadata to control how prerendered content is served:

prerender do
  get "/about", PageController, :about             # served to everyone
  live "/changelog", ChangelogLive                  # not prerendered (just live)
  live "/status", StatusLive, :index,
    metadata: %{prerender: :bots_only}              # prerendered for SEO bots only
  live "/yolo", PreRenderedLive, :index,
    metadata: %{prerender: :always, isr: true}      # prerendered for everyone + ISR
end
Mode Behavior
prerender: true Served to everyone (default for dead views)
prerender: :bots_only Served only to search engine crawlers; browsers get the live app
prerender: :always Served to everyone with fresh session/CSRF (requires session_options)

Incremental Static Regeneration

ISR implements the stale-while-revalidate pattern: stale pages are served immediately while a background task re-renders and writes fresh HTML. Users never wait. ISR is opt-in per route via isr: true in metadata:

live "/status", StatusLive, :index,
  metadata: %{prerender: :always, isr: true}
Thundering herd prevention: ETS-based locks via insert_new/2 ensure only one task regenerates a given path at a time. The lock is released automatically when the task completes.

See it in action on the /status page.

LiveView CSRF Token Swap

For prerender: :always routes, the plug establishes a fresh session and replaces the stale build-time CSRF token in the prerendered HTML before serving. This allows LiveView's WebSocket to connect successfully without infinite refresh loops.

# endpoint.ex — pass session options to enable CSRF swap
@session_options [store: :cookie, key: "_app_key", signing_salt: "..."]

plug PhoenixPrerender.Plug, session_options: @session_options

Routes with :bots_only don't need this — bots don't run JavaScript, so there's no WebSocket connection to worry about.

<.prerendered> Component

PhoenixPrerender.Components provides a component that freezes content at prerender time. When LiveView hydrates, the prerendered DOM value is preserved — LiveView won't patch it.

import PhoenixPrerender.Components

# Frozen at prerender time
<.prerendered id="gen-time" tag="p" class="font-mono">
  {@generated_at}
</.prerendered>

# Updates normally via WebSocket
<p>{@current_time}</p>

Uses phx-update="ignore" under the hood. See the /status page for a live demo.

Distributed Regeneration

When running on multiple BEAM nodes, :global.trans/2 ensures only one node regenerates a given page. After regeneration, Phoenix PubSub broadcasts to all nodes so they invalidate their local ETS caches.

If the regenerating node crashes, :global automatically releases the lock so another node picks up the work.

Atomic File Writes

Every file write is a two-step process: write to path.tmp, then File.rename!/2 to the final location. This is critical during ISR — the serving plug never reads a partially written file, even when pages are being regenerated while being served.

Smart Serving Plug

PhoenixPrerender.Plug sits in your endpoint before the router. The serving order is:

  1. 1. Manifest check — per-route prerender_mode and ISR settings
  2. 2. Bot detection — for :bots_only routes, check User-Agent
  3. 3. CSRF swap — for :always routes, inject fresh session + token
  4. 4. ETS cache — in-memory lookup via PageCache
  5. 5. Disk — check for static HTML file
  6. 6. Pass-through — fall to Phoenix router

Strict paths mode (default) only serves pages listed in manifest.json, preventing arbitrary file serving. Path traversal attacks are rejected.

Telemetry

Four event families for full observability:

  • [:phoenix_prerender, :generate]
  • [:phoenix_prerender, :render]
  • [:phoenix_prerender, :serve]
  • [:phoenix_prerender, :regenerate]

Manifest & Sitemap

After generation, two files are written:

  • manifest.json — per-route prerender_mode, ISR flag, checksums, sizes
  • sitemap.xml — standard sitemaps.org format