For nearly every meaningful project I've taken on, there's always been a landing page first to help me market and semi-validate an idea.
Tons of tools exist to capture email and market to that audience, but I've always been bothered by the lack of control of that data and how we blindly hand it over to services like Mailchimp, Mailerlite, ConvertKit, etc...
Those tools save you time and enable you to focus on your project, but I commonly like to keep that stuff in-house. That way, as my project scales, so can my marketing tactics.
Collecting a simple email address for the purpose of a newsletter seems super downright and straightforward overkill for Rails. Still, in this guide, I’ll show you some techniques to make the process snappy and more delightful than the legacy ways involving page refreshes and redirects.
Create a vanilla rails app
rails new hotwire_newsletters
Install Rails UI - Optional
I made Rails UI to help save myself and your time. You are more than welcome to skip this step.
bundle add railsui --github getrailsui/railsui --branch main
With Rails UI installed, you’ll need to boot your server bin/dev
and visit localhost:3000
. From there, you may configure your Rails UI installation in a couple of clicks. Don't worry about installing any pages for now.
The next simplified steps are
- Choose a CSS framework
- Choose a theme
- Click “Save changes”
Generate a Subscriber resource
It's worth noting that in many apps I’ve built, the model Subscriber
is used for other things like billing, so to keep that from being an issue, you might name your resource NewsletterSubscriber
or something similar.
A scaffold is overkill for our need, but I’m more focused on speed. We’ll clean up the cruft this generates in a later step.
rails g scaffold Subscriber email
Migrate the database and create the new Newsletter
table.
rails db:migrate
Update routing
The default routing at this stage points to the Rails UI start page. Let’s change that to be a new page on a new pages_controller.rb
. If you don't have a pages_controller.rb
, you can make one manually or run rails g controller pages home
.
Rails.application.routes.draw do
resources :newsletters
if Rails.env.development? || Rails.env.test?
mount Railsui::Engine, at: "/railsui"
end
# Inherits from Railsui::PageController#index
# To override, add your own page#index view or change to a new root
# Visit the start page for Rails UI any time at /railsui/start
# root action: :index, controller: "railsui/page"
devise_for :users
# Defines the root path route ("/")
root "pages#home"
end
If using Rails UI, comment out the existing root path and add the new one at the bottom.
Update pages_controller file
Create a page_controller.rb
file in app/controllers
and a home
action within that controller. Add the corresponding app/views/pages
directory with a home.html.erb
template as well.
class PagesController < ApplicationController
def home
@hide_nav = true
end
end
For this guide, I’ve added an instance variable called @hide_nav
. The nav feels like a distraction since we want to focus more on the subscriber form. We'll use this in the application layout file to not render the nav if it's set to true
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>HotwireNewsletterSubscription</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
<%= stylesheet_link_tag "application", "https://unpkg.com/trix@2.0.0/dist/trix.css", "data-turbo-track": "reload" %>
<%= render "shared/fonts" %>
<%= render "shared/meta" %>
<%= yield :head %>
</head>
<body class="rui">
<%= render "shared/flash" %>
<% if content_for(:header).present? %>
<%= yield :header %>
<% else %>
<%= render "shared/nav" unless @hide_nav %>
<% end %>
<%= yield %>
<%= railsui_launcher if Rails.env.development? %>
</body>
</html>
You should now see the home page as the root page in your app without a navigation in sight.
Dialing in the subscribe page
I’m going to treat the “home” page as a simple subscribe page for this guide.
In the view, we’ll render a primary container with a form. Notice the turbo_frame_tag
with the ID of newsletter.
Also, notice the src
attribute, which dynamically renders the view on the other end of the path you include.
<!-- app/views/page/home.html.erb -->
<div class="flex flex-col justify-center items-center h-screen bg-slate-50 border-t">
<div class="w-[460px] mx-auto rounded-xl bg-white p-8 shadow border border-slate-300">
<div class="mb-6">
<h4 class="tracking-tight">Subscribe to our newsletter</h4>
<p class="my-6 text-slate-700">Tips based on proven scientific research.</p>
</div>
<%= turbo_frame_tag "newsletter", src: new_subscriber_path %>
</div>
</div>
new_subscriber_path
points to app/views/subscribers/new.html.erb
. So, the contents of that page essentially render in place if there's another turbo_frame_tag in that file. Pretty slick!
An important thing to note is that the new.html.ere
template only contains the form but is surrounded by a familiar turbo_frame_tag
with the same ID as we used on the home.html.erb
template. Without this, the view won't render correctly.
<!-- app/views/newsletter_subscribers/new.html.erb-->
<%= turbo_frame_tag "newsletter" do %>
<%= render "form", subscriber: @subscriber %>
<% end %>
I updated the design of the _form.html.erb
partial slightly from what gets generated with Rails UI. You can find this in app/views/subscribers/_form.html.erb
.
<!-- app/views/subscribers/_form.html.erb-->
<%= form_with(model: subscriber) do |form| %>
<%= render "shared/error_messages", resource: form.object %>
<div class="form-group">
<%= form.label :email, class: "form-label" %>
<%= form.text_field :email, class: "form-input", placeholder: "My email address is" %>
</div>
<div class="flex items-center justify-between flex-wrap">
<div class="sm:flex-1 sm:mb-0 mb-6">
<%= form.submit class: "btn btn-primary w-full" %>
</div>
</div>
<% end %>
Remove the cruft
The scaffold we ran generated a lot of fluff that we didn’t need. I removed every view file in app/views/subscribers
except for the _form.html.erb
partial and the new.html.erb
template within app/views/subscribers
Simplifying the Subscribers Controller
The scaffold generator gives all the CRUD actions you might use in a typical Rails controller, but we're not using most of them. Much like the view files, I simplified the controller for our purposes and reduced everything to create
and new
actions. That leaves us with only a few lines of ruby.
class SubscribersController < ApplicationController
def new
@subscriber = Subscriber.new
end
def create
@subscriber = Subscriber.new(subscriber_params)
unless @subscriber.save
render :new
end
end
private
def subscriber_params
params.require(:subscriber).permit(:email)
end
end
Pay attention to the create
action, which is where the magic lies.
Unless there’s an issue saved successfully, we’ll render the new.html.erb
template.
If the newsletter subscriber does save, Rails knows to check for a create.html.erb
file in a last-ditched effort to render something as a response. We can use turbo frames to create a new view showing a proper success state that effectively re-renders the view with updated content in real time.
Create a new view called create.html.erb
in app/views/subscribers
. I modified an alert component inside this template that ships with Rails UI to display a success banner and message.
<!-- app/views/subscribers/create.html.erb-->
<%= turbo_frame_tag "newsletter" do %>
<div class="bg-green-50/90 text-green-700 p-4 rounded text-sm sm:flex items-center justify-between">
<div class="flex items-start justify-between space-x-3">
<%= icon "check", classes: "text-green-600 w-5 h-5 flex-shrink-0" %>
<div class="flex-1">
<p class="text-green-800 font-semibold">Successfully subscribed</p>
<p class="leading-snug my-1">Look for a confirmation email from us in your inbox soon.</p>
</div>
</div>
</div>
<% end %>
Notice we’re leveraging the same turbo_frame_tag
with the newsletter
ID being passed. This is important.
When you add a valid email address and submit the form, you should see the success state instantly.
What about error states and validations?
This is relatively easy, assuming you’re already rendering proper form errors in your form.
I added a basic validation to the Subscriber
model to ensure a value for email
when the form is submitted. If not, we'll display errors.
class Subscriber < ApplicationRecord
validates :email, presence: true
end
Rails Ul comes pre-styled with error handling, but feel free to modify this in app/views/shared/_error_messages.html.erb
. You can also customize what error messages return on the validation itself.
What about spam?
Good question. There are a lot of bots that will game your forms. One quick win that has helped me is using a CAPTCHA for all publicly accessible forms. A great one that integrates with Rails is called invisible_captcha. It's a simple gem and is simple to use. Perhaps I'll do a quick tutorial on it in the future.
Useful links
- The web-crunch Hotwire and Rails collection
- Hotwire home page
- Github Repo for this Guide
- Subscribe to my YouTube channel
Top comments (0)