https://blog.appsignal.com/2025/12/10/create-a-markdown-editor-in-ruby-on-rails.html
* APM
Core features
+
Error Tracking
+
Performance Monitoring
+
Host Monitoring
+
Anomaly Detection
+
Uptime Monitoring
+
Metric Dashboards
+
Log Management
+
Check-ins
Advanced tools
+
Workflow
+
Automated Dashboards
+
Time Detective
+
MCP Server
* Languages
Supported Languages
+
Ruby (on Rails) APM
+
Elixir APM
+
Node.js APM
+
JavaScript Error Tracking
+
Python APM
+
OpenTelemetry
* Business
Larger scale
+
Enterprise
Add-Ons
+
Long-Term Log Storage
+
HIPAA Compliance
+
SAML SSO
* Docs
* Learn
* Blog
* Pricing
* Login
* Start free trial
Menu
Core features
* Error Tracking
* Performance Monitoring
* Host Monitoring
* Anomaly Detection
* Uptime Monitoring
* Metric Dashboards
* Log Management
* Check-ins
Advanced tools
* Workflow
* Automated Dashboards
* Time Detective
* MCP Server
Supported Languages
* Ruby (on Rails) APM
* Elixir APM
* Node.js APM
* JavaScript Error Tracking
* Python APM
* OpenTelemetry
Larger scale
* Enterprise
Add-Ons
* Long-Term Log Storage
* HIPAA Compliance
* SAML SSO
* Learn
* Docs
* Blog
* Pricing
* Login
* Start free trial
ruby
Create a Markdown Editor in Ruby on Rails
Hans-Jorg Schnedlitz
Hans-Jorg Schnedlitz on Dec 10, 2025
Create a Markdown Editor in Ruby on Rails
On this page
* Markdown in Ruby on Rails
* Markdown and Markdown Flavors
* Creating a Markdown Editor with Ruby on Rails
* Preview with Turbo Streams
* Improving the User Experience
* Adding Image Uploads to our Markdown Editor in Ruby on Rails
* Wrap Up
Share this post
Is your app broken? AppSignal lets you know.
Monitoring by AppSignal
In recent years, Markdown has become the lingua franca of plain-text
files on the web. If you're a developer, you have read -- and maybe
even written -- hundreds of Markdown documents over the course of your
career.
GitHub repositories use README files written in Markdown. Stack
Overflow and Reddit use it to format posts. Technical documentation,
blog posts, and entire books are written in Markdown. And it's not
just for humans either! AI tools such as Claude Code and Cursor use
Markdown documents to improve the effectiveness of AI agents. This
article you are reading is -- you guessed it -- written in Markdown!
Ruby on Rails, of course, has its own tooling around Markdown, and in
this post, we'll build a Markdown editor using Rails.
Markdown in Ruby on Rails
Rails 8.1 brings Markdown as content type, as well as a new rich text
editor with Markdown support. And if you can't or don't want to use
Rails defaults, there are always gems, such as Marksmith.
But what does it actually take to build a GitHub-like Markdown
editor? Something simple that supports editing Markdown text and
previewing the result. Something like this:
Editor
Preview
Let's find out!
Markdown and Markdown Flavors
Before we start with the implementation, let's talk about the
Markdown language itself. First, it's essential to understand that
there is no single, definitive Markdown language. When Markdown was
conceived in 2004, its original description left quite some room for
interpretation. As a result, a plethora of Markdown dialects -- or
flavors -- sprang into existence, each of them supporting different
features and slightly different syntaxes.
The flavor you might be most familiar with is GitHub Flavored
Markdown, or GFM in short. It is based on CommonMark, an attempt to
create a formal, unified specification for Markdown, which was
created in 2014. The CommonMark specification has been widely
adopted, but even so, several different Markdown flavors remain
common today.
Now, what makes Markdown so interesting, and why was it so widely
adopted? The beautiful thing about Markdown is that it strikes a
perfect balance between being structured and unstructured. It is not
as verbose as other, more structured languages (such as XML, HTML, or
JSON), and that makes it easy to read and write. But it provides just
enough structure to be processed by computers. And that allows us to
do interesting things with it.
One of those interesting things is converting Markdown to other
formats, such as HTML. As a matter of fact, that was exactly what its
designers had in mind!
Markdown allows you to write using an easy-to-read, easy-to-write
plain text format, then convert it to structurally valid HTML.
Source: Markdown, John Gruber's website
Using Ruby, we have several fine options for converting Markdown into
HTML: redcarpet, commonmarker, and Kramdown, to name a few. What they
all have in common is that they convert Markdown to HTML. For
example:
Markdown: Markdown supports *italic* and **bold** text.
HTML:
Markdown supports italic and bold
text.
As I mentioned earlier, different flavors of Markdown work
differently. We'll be using GitHub Flavored Markdown (GFM) for the
rest of this article. If you're curious about how GFM differs from
other Markdown flavors, take a look at the specification. Note the
sections tagged as extension -- these are features that are unique to
GFM and not usually found in other Markdown dialects.
Now that we have covered the theory, let's get building.
Creating a Markdown Editor with Ruby on Rails
Our Markdown editor will require three things. First, a way for users
to write -- and save -- plain text. This is easily accomplished using
off-the-shelf Rails features. Second, we'll utilize a third-party gem
to render Markdown to HTML. And finally, a tiny bit of JavaScript to
tie these things together and create a nice user experience.
While the quickest way to add a Markdown editor to your application
is simply by using a gem -- such as the excellent Marksmith -- it's
always worthwhile to learn what goes on under the hood. You can
follow along with the code used in this post by viewing or cloning
this repository.
Let's start a new Rails app with a simple post model that will hold
our Markdown text. We use Tailwind CSS for styling and also enable
ActiveStorage. Why do we need ActiveStorage, you ask?
It's a surprise: you'll see later.
Shell
rails new github-markdown-editor --css=tailwind && cd github-markdown-editor
rails active_storage:install
# Add our Post model and migrate
rails g scaffold Post body:text
rails db:migrate
The post scaffold creates a form with a text area, which is all we
need for users to write and save Markdown documents. Now we need to
render those documents to HTML. We'll use Commonmarker. It supports
GitHub Flavored Markdown and is easily customizable, which makes it
the logical choice.
Shell
bundle add commonmarker
Before we tackle the editor itself, let's update our app so it
displays rendered Markdown when showing a post. We only need to add a
handful of lines to both our posts controller and the post partial.
Note our use of html_safe. Otherwise, the HTML created by
Commonmarker will be sanitized by Rails. We don't want sanitized HTML
-- we want our HTML to be shown to the user as rendered.
Ruby
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def show
require 'commonmarker'
@markdown = Commonmarker.to_html(@post.body).html_safe
end
#...
end
erb
Markdown:
<%= @markdown %>
To try it out, let's create a new post with the following Markdown
content.
md
# Markdown Rendering
Let's add some Markdown rendering to a Rails application using the [Commonmarker](https://github.com/gjtorikian/commonmarker) gem.
Markdown supports **bold** and _italic_ text. GFM adds features like ~~strike through text~~.
Autolinking and emojis are supported by Commonmarker, check it out: https://www.appsignal.com/ :heart:
There's also syntax highlighting:
```ruby
def greet(name)
puts "Hello, #{name}"
end
```
As well as tables and lots of other features!
| Gem | Main Feature |
| ------------ | ------------------------------------------- |
| CommonMarker | GitHub-flavored Markdown |
| AppSignal | Performance monitoring for your application |
If we view the post, things are clearly working. We can see bold and
italic text, links, and even syntax highlighting. But we are still
far from a GitHub-like editor.
We don't want to create a post to see what the resulting HTML looks
like. What we need is a live preview.
Preview with Turbo Streams
Rather than showing rendered HTML only after a post is created, we
want to display it while in the writing process. We'll easily be able
to do so using Turbo Streams and StimulusJS. Let's add a Turbo stream
action to our posts controller and the corresponding route to our
routes file.
Ruby
# app/controllers/posts_controller.rb
class PostController
def preview
require "commonmarker"
@markdown = Commonmarker.to_html(params[:body])
render turbo_stream: turbo_stream.update("preview", @markdown)
end
# ...
end
Ruby
# config/routes.rb
Rails.application.routes.draw do
resources :posts do
post :preview, on: :collection
end
# ...
end
Our Turbo stream targets the preview element. Accordingly, we create
a div with the preview ID in the post form. We also add a button that
will trigger the Turbo stream action using a Stimulus controller --
which we'll create next.
erb
<%= form_with(model: post, class: "contents",
data: {
controller: "preview",
preview_url_value: preview_posts_path
}) do |form| %>
| Article continues below
Left squiggle
Is your app broken or slow? AppSignal lets you know.
Monitoring by AppSignal -
Right squiggle
Our Stimulus controller is nothing special. It takes the content of
our editor, which we refer to as the editorContentTarget. It then
sends a request to the post preview action. The rest is done
automagically by Turbo. On a side note, we are using rails/request.js
here, which makes it a lot simpler to send Turbo Stream requests -- or
any other requests for that matter -- to your Rails application from
JavaScript.
JavaScript
// app/javascript/controllers/preview_controller.js
import { Controller } from "@hotwired/stimulus";
import { post } from "@rails/request.js";
export default class extends Controller {
static targets = ["editorContent"];
static values = { url: String };
show() {
post(this.urlValue, {
body: {
body: this.editorContentTarget.value,
},
responseKind: "turbo-stream",
});
}
}
In our post form, we can now preview the rendered Markdown at the
click of a button. Great success!
Improving the User Experience
Now, there are two more things to address. For one, if you've been
playing along, you've probably noticed our HTML doesn't really look
right. We are rendering tags such as h1 or table correctly, but the
styling is off. Since we use Tailwind, there is an easy way to fix
this -- the TailwindCSS typography plugin.
css
/* app/assets/tailwind/application.css */
@import "tailwindcss";
/* Add the typography plugin */
@plugin "@tailwindcss/typography";
What does it do, you ask?
The official Tailwind CSS Typography plugin provides a set of
prose classes you can use to add beautiful typographic defaults
to any vanilla HTML you don't control, like HTML rendered from
Markdown, or pulled from a CMS. Source: Tailwind CSS Typography
GitHub README
Sounds perfect for our purpose. To make our HTML look pretty, we just
need to add the prose class to our preview pane.
html
Without prose
With prose
Much better. If you are not using Tailwind or the typography plugin,
you can still make your HTML look nice, but you'll need to create the
necessary CSS by hand.
Now, finally, let's touch up our editor. We want to switch between
our editor (the text area where we write our Markdown) and our
preview at the click of a button. For that, we'll update our Stimulus
controller so that it toggles between these two panes by hiding one
and displaying the other. We'll introduce the necessary targets,
along with new targets for the buttons (so we can change the style of
the active one).
JavaScript
// app/javascript/controllers/preview_controller.js
export default class extends Controller {
static targets = [
"editor",
"editorContent",
"preview",
"editorButton",
"previewButton",
];
static values = { url: String };
editor(event) {
event.preventDefault();
this.#toggleButton(this.editorButtonTarget, this.previewButtonTarget);
this.#togglePane(this.editorTarget, this.previewTarget);
}
preview(event) {
event.preventDefault();
post(this.urlValue, {
body: { body: this.editorContentTarget.value },
responseKind: "turbo-stream",
});
this.#toggleButton(this.previewButtonTarget, this.editorButtonTarget);
this.#togglePane(this.previewTarget, this.editorTarget);
}
#toggleButton(activate, deactivate) {
// Toggle button styles
activate.classList.remove("bg-gray-100", "hover:bg-gray-50");
// ...
}
#togglePane(show, hide) {
// Hide one pane and show the other
show.classList.remove("hidden");
hide.classList.add("hidden");
}
}
We also need to make some changes to our form. We can remove our
previous preview button since we no longer need it.
erb
And that's all we need to create a magnificent Markdown editor just
like the one GitHub has! Well, that's not entirely true. GitHub's
editor has a lot of subtle features that make our writing experience
more pleasant -- we won't add all of them here.
There's one thing we will add, though. What I adore about the GitHub
editor is that it lets you add images to your Markdown simply by
pasting them into the editor. Let's wrap up this article by adding
this advanced feature.
Adding Image Uploads to our Markdown Editor in Ruby on Rails
Remember how we enabled ActiveStorage when we set up our demo
application? Well, this is what it's for!
To upload files directly from our editor, we'll be using Rails Direct
Uploads. Let's add the necessary JavaScript to our application.
Shell
bin/importmap pin @rails/activestorage
Next, we update our trusty preview controller one final time.
JavaScript
import { DirectUpload } from "@rails/activestorage";
export default class extends Controller {
// Add the upload URL value
static values = { url: String, uploadUrl: String };
upload(event) {
if (!event.clipboardData.files.length) return;
event.preventDefault();
Array.from(event.clipboardData.files).forEach((file) =>
this.#uploadFile(file)
);
}
#uploadFile(file) {
const upload = new DirectUpload(file, this.uploadUrlValue);
upload.create((_error, blob) => {
const url = `/rails/active_storage/blobs/redirect/${
blob.signed_id
}/${encodeURIComponent(blob.filename)}`;
const link = `\n`;
// Update editor content
const start = this.editorContentTarget.selectionStart;
const end = this.editorContentTarget.selectionEnd;
this.editorContentTarget.setRangeText(link, start, end);
});
}
// ...
}
The upload function will be called whenever a user pastes something
in our editor. If the pasted data contains any files, we use the
DirectUpload class supplied by ActiveStorage to upload them to our
server. Once the upload has finished, we take the resulting blob
attributes and convert them into a markdown image link that
Commonmarker can convert to HTML. Finally, we use selectionStart and
selectionEnd to determine at which position to insert this link.
We now only need to update our HTML to provide the direct upload URL
and wire up the paste event to our Stimulus controller. Simple.
erb
<%= form_with(model: post, class: "contents", data:
{ controller: "preview",
preview_url_value: preview_posts_path,
preview_upload_url_value: rails_direct_uploads_url
}) do |form| %>
<%= form.textarea :body,
data: {
preview_target: "editorContent",
action: "paste->preview#upload",
},
%>
%>
Now, we did leave some things out. For one, there is no error
handling -- your image upload may fail, and you should handle that.
Also, for a true GitHub-like experience, you'd want to support
uploading files via drag-and-drop, as well as allowing users to
select files using a file picker. These features work similarly to
upload-on-paste -- we just left them out in this post for brevity.
Wrap Up
Markdown is arguably the language of the web. Except it isn't a
single language, but is comprised of various dialects, the most
popular of which are CommonMark and GitHub Flavored Markdown.
In the Ruby ecosystem, Commonmarker is an excellent choice for
converting GFM to HTML.
By leveraging the power of Turbo Streams, StimulusJS, and
TailwindCSS, we have created a nice-looking Markdown editor that
supports live previews and image uploads in no time.
Happy coding!
Wondering what you can do next?
Finished this article? Here are a few more things you can do:
* Share this article on social media
Most popular Ruby articles
*
What's New in Ruby on Rails 8
What's New in Ruby on Rails 8
Let's explore everything that Rails 8 has to offer.
See more
*
Measuring the Impact of Feature Flags in Ruby on Rails with
AppSignal
Measuring the Impact of Feature Flags in Ruby on Rails with
AppSignal
We'll set up feature flags in a Solidus storefront using Flipper
and AppSignal's custom metrics.
See more
*
Five Things to Avoid in Ruby
Five Things to Avoid in Ruby
We'll dive into five common Ruby mistakes and see how we can
combat them.
See more
Hans-Jorg Schnedlitz
Hans-Jorg Schnedlitz
Our guest author Hans is a Rails engineer from Vienna, Austria. He
spends most of his time coding or reading about coding, and sometimes
even writes about it on his blog! When he's not sitting in front of a
screen, you'll probably find him outside, climbing some mountain.
All articles by Hans-Jorg Schnedlitz
Become our next author!
Find out more
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix,
Node.js, Express and many other frameworks and libraries. We are
located in beautiful Amsterdam. We love stroopwafels. If you do too,
let us know. We might send you some!
Discover AppSignal
AppSignal monitors your apps
AppSignal monitors your apps
Features
* Error Tracking
* Performance Monitoring
* Host Monitoring
* Anomaly Detection
* Uptime Monitoring
* Metric Dashboards
* Workflow
* Log Management
* Automated Dashboards
* Check-ins
* Time Detective
* MCP Server
Resources
* Plans & pricing
* Documentation
* Blog
* Customer Stories
* Changelog
* Learning Center
* Why AppSignal
Compare
* vs Better Stack
* vs Datadog
* vs Honeybadger
* vs New Relic
* vs Scout APM
* vs Sentry
* vs SolarWinds
Support
Do you need help, have a feature request or just need someone to
rubber duck with? Get in touch with one of our engineers.
* Contact us
* Live chat
* Status
* Security
* Enterprise
* Long-Term Log Storage
* HIPAA Compliance
* SAML SSO
About us
AppSignal is located in the beautiful Netherlands. We love
stroopwafels. If you do too, let us know. We might send you some!
* About
* Jobs
* Write for Our Blog
* Diversity
* Open Source
* X
* Discord
Languages
* Ruby
Active Record, Capistrano, Delayed::Job, Garbage Collection,
Global VM Lock, Grape, GraphQL, Hanami, MongoDB, Padrino, Puma,
Que, Ruby on Rails, Rake, Resque, Shoryuken, Sidekiq, Sinatra,
Solid Queue, ViewComponent, Webmachine
* Elixir
Absinthe, Ecto, Erlang, Finch, Oban, Phoenix, Plug, Tesla
* Node.js
AMQPlib, Apollo Gateway, BullMQ, Express, Fastify, GraphQL,
Knex.js, Koa, MongoDB, Mongoose, MySQL, NestJS, Next.js, Pino,
PostgreSQL, Prisma, Redis, Remix, Restify, Winston
* JavaScript
React, Vue, Angular, Ember, Preact, Stimulus
* Python
Celery, Django, FastAPI, Flask, Jinja2, MySQL, Pika, PostgreSQL,
Redis, Request, SQLAlchemy, SQLite, Starlette, WSGI and ASGI
* OpenTelemetry
Elixir, Go, Java, Laravel, Next.js, Node.js, PHP, Python, Ruby,
Rust, Symfony, Vercel
* Terms & Conditions
* Privacy Policy
* Cookie Policy
* GDPR compliance
* Contact us / Imprint