class PostsController < ApplicationController
def create
@post = current_member.posts.build(post_params)
if @post.save
flash.now[:notice] = 'Post created.'
respond_to do |format|
format.turbo_stream
format.html { redirect_to @post, notice: 'Post created.' }
end
else
flash.now[:alert] = 'Please fix the errors.'
respond_to do |format|
format.turbo_stream { render :create, status: :unprocessable_entity }
format.html { render :new, status: :unprocessable_entity }
end
end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
end
<%= turbo_stream.replace 'flash' do %>
<%= render 'shared/flash' %>
<% end %>
<% if @post.persisted? %>
<%= turbo_stream.prepend 'posts' do %>
<%= render @post %>
<% end %>
<% else %>
<%= turbo_stream.replace 'post_form' do %>
<%= render 'form', post: @post %>
<% end %>
<% end %>
Instead of sprinkling custom JS for notifications, I treat flash as UI state and render it via Turbo Streams. When a create/update succeeds, the controller responds to format.turbo_stream and the template uses turbo_stream.replace to swap the flash container. This keeps the behavior consistent for normal HTML requests and Turbo visits, and it also avoids the common bug where flash only appears after a redirect. I like to keep the flash DOM stable with a single #flash wrapper and let the server decide content. It’s straightforward to extend with different levels (notice, alert) and auto-dismiss later via Stimulus.