Caching
Fragment caching works on Poetry markup the way it works on any
server-rendered HTML: cache the fragment, nest caches inside caches,
let touch cascades invalidate
what changed. One blind spot needs a recipe of its own, and a few
rules keep fragments cacheable at all.
The digest blind spot
Rails invalidates fragment caches through template digests: each template's cache keys include a hash of its own source and of every template it renders. That mechanism cannot see Poetry, twice over:
-
Discovery. The dependency tracker scans templates
for
rendercalls.poetry_cardis a helper call, so the component is never registered as a dependency. - Resolution. Poetry's templates live in the gem, outside your app's view paths. Even a discovered dependency could not be resolved into the digest tree.
The consequence is silent: a cached fragment containing Poetry markup is not invalidated when you upgrade poetry. After an upgrade that changed a component's template or class, any fragment cached before it keeps serving the old markup. No error, no warning, stale UI.
The recipe: version-keyed cache keys
Put the Poetry version in the cache key of any fragment whose markup includes Poetry helpers:
<% cache [@post, Poetry::Ui::VERSION] do %>
<%= poetry_card(key: @post) do |card| %>
<% card.with_title { @post.title } %>
...
<% end %>
<% end %>
-
Every Poetry upgrade changes
Poetry::Ui::VERSION, so every version-keyed fragment misses once and re-renders fresh. - With nested (russian-doll) caches, the version key belongs on every fragment that directly contains Poetry markup. An inner fragment's key does not protect an outer one that also renders helpers.
-
Charts too: fragments containing
poetry_chartaddPoetry::Charts::VERSION.
A small app-side helper keeps call sites short:
# app/helpers/application_helper.rb
def cache_with_poetry(*keys, &block)
cache([*keys, Poetry::Ui::VERSION], &block)
end
Development note: with a path-pinned Poetry checkout
the version constant does not change per edit. Leave
rails dev:cache off while iterating on components (the
default), or clear the cache after gem edits.
Rules for markup that can be cached at all
These rules are what make russian-doll caching work in practice, stated for poetry:
-
No viewer conditionals inside the fragment. A
cached fragment is the same bytes for every viewer.
if current_user.admin?inside a cached fragment poisons the cache for everyone else: split the fragment, or move the decision out of the cacheable markup. -
Personalization is decoration. Render the shared
markup once, cache it, and let a thin client layer decorate per
viewer:
data-*attributes plus a Stimulus controller reading client state. Poetry's own color scheme works exactly this way; the cached markup is theme-neutral, andpoetry_color_scheme_scriptapplies the viewer's mode at paint time. - Timestamps and counters stay out, or get their own fragment. Anything per-request (relative times, unread counts) renders outside the cache, in its own smaller fragment, or as decoration.
-
touch: trueup the ownership chain is what makes record-keyed invalidation cascade. The Poetry version key composes with it; it never replaces it.
Components you copied in
Components copied into the app with poetry:add live
under app/components and are inside your digest tree,
but discovery still fails because they render through helpers. The
standard Rails escape hatch works: a template dependency magic
comment in each template that renders the copy-in, which the
digestor resolves like any other dependency.
<%# Template Dependency: components/button/component %>
<% cache @post do %>
...
<% end %>
The version-key recipe also covers copy-ins during Poetry upgrades
that re-copy; poetry:diff tells you when the shipped
source moved.
Ids inside cached fragments
Fragment caching replays HTML that was rendered once, including every DOM id frozen into it at render time. The invariant that makes that safe:
Cached HTML is safe only when every id frozen into it remains unique under every composition and replay of that cached HTML.
Random ids satisfy uniqueness on the first render but cannot be
paired by Turbo morph afterwards, and the same cached fragment
rendered twice on one page duplicates even its random ids. So the
rule is: Poetry components inside a cache block
take key: (a record, or a literal) or an explicit
id:. A keyed component's HTML is a pure
function of its inputs, so cached copies and fresh renders can
never disagree.
poetry:check warns on unkeyed components in cache
blocks (stable-identity/cache), and
poetry_id_integrity_script in your development layout
catches what static analysis cannot: the composed page. The same
rule covers every other render that escapes the request cycle,
such as broadcast partials destined for
turbo_stream.morph and pre-rendered HTML persisted
anywhere. The full identity story is the
Stable IDs guide.
HTTP caching, ETags, and CSRF
Three separate mechanisms, often conflated:
-
Body-hash 304s (
Rack::ETag) need byte-identical responses. Poetry's keyed ids remove the component-id churn, butcsrf_meta_tagsmasks a fresh token every render, so the standard layout makes even formless pages vary. The realistic scope: responses with no request-varying output at all. -
fresh_when/stale?compute validators from records before rendering; Poetry ids are irrelevant to their hit rate, and they are the right 304 mechanism for form pages. Rails CSRF tokens are per-session, so an older masked token inside a browser-reused page still submits. Two cautions: a session reset invalidates tokens inside browser-cached pages (represent such dependencies via theetag:hook), andper_form_csrf_tokens, when enabled, scopes tokens to a form's action and method. -
Fragment caching and forms: never cache the token.
Keep the
formtag outside the cached fragment and cache the expensive content within. A cached hidden token field serves one session's token to every visitor.
<%= form_with model: @post do |form| %>
<% cache [@post, Poetry::Ui::VERSION] do %>
...the expensive field markup...
<% end %>
<% end %>