ruby erb 89 lines · 4 tabs

Inline markdown preview using Turbo Frames

Shared by codesnips Jan 2026
4 tabs
class PostsController < ApplicationController
  def new
    @post = Post.new
  end

  def create
    @post = current_user.posts.build(post_params)
    if @post.save
      redirect_to @post
    else
      render :new, status: :unprocessable_entity
    end
  end

  def preview
    @rendered = MarkdownRenderer.render(params[:content].to_s)

    respond_to do |format|
      format.turbo_stream
      format.html { render partial: "posts/preview", locals: { rendered: @rendered } }
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :content)
  end
end
4 files · ruby, erb Explain with highlit

This snippet shows how a live markdown preview is built with Turbo Frames instead of a client-side markdown parser, keeping the rendering logic on the server where it can be sanitized consistently. The idea is that the textarea's contents are periodically POSTed to a preview action, which renders the compiled HTML back into a lazy-loading Turbo Frame. Because the frame swaps its own contents on every matching response, the editor updates in place without a full page reload and without duplicating markdown rules in JavaScript.

In PostsController, the preview action never touches the database: it reads params[:content], runs it through MarkdownRenderer.render, and responds with a small partial. The response is wrapped in a turbo_frame_tag with the id markdown_preview, so Turbo matches it to the frame already on the page and replaces only that fragment. Rendering server-side lets a single trusted pipeline handle both preview and the eventual saved post, avoiding drift between what an author sees and what readers get.

MarkdownRenderer wraps Redcarpet with the html_safe-aware pattern that matters most: it renders markdown to HTML and then passes the result through Rails' sanitize helper via an ActionController::Base.helpers proxy. This is the key trade-off — markdown can emit arbitrary HTML, so the output is treated as untrusted and stripped to an allowlist of tags and attributes before being marked safe. fenced_code_blocks and autolink are enabled for a realistic authoring experience.

The _form.html.erb tab ties it together. The form_with targets the preview action, data-turbo-frame points responses at the frame, and a small Stimulus controller reference (data-controller="preview") debounces input so a request fires roughly every 400ms rather than on every keystroke. The turbo_frame_tag "markdown_preview" holds the last rendered fragment.

A pitfall worth noting: without debouncing, fast typing floods the server with requests, and out-of-order responses can flicker the preview. Turbo requests to the same frame are also naturally superseded, which mitigates stale renders. This pattern suits any editor where correctness and shared rendering rules outweigh the latency of a network round-trip.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Inline markdown preview using Turbo Frames — share card
Link copied