Part of putting together a paid courses is giving people an idea of what content they’re be getting, but not so much where there’s no point in them signing up to support your efforts.
Here’s what the paywall looks like so far on my Phlex for Rails course.
Let’s take a closer look.
Markdown redactor
At the core of the paywall is a parser that replaces characters with a unicode placaholder. It parses the output of the markdown HTML and splits it into a preview, which people can see, and redacted content, which has the unicode ▓ charactors.
# ./lib/paywall.rb
class Paywall
REDACTED_CHARACTER = "▓"
attr_reader :preview, :redacted
def initialize(html)
@dom = Nokogiri::HTML.fragment(html)
@preview = slice_nodeset(@dom, 0...5).to_html.html_safe
@redacted = redact(slice_nodeset(@dom, 5..-1)).to_html.html_safe
end
private
def redact(dom)
dom.css("p").each do |element|
element.content = redact_text element.content
end
dom.css(".locked").each do |element|
element.content = redact_text element.content
end
dom
end
def slice_nodeset(dom, range)
nodes = dom.children.to_a
slice = nodes[range] || []
Nokogiri::XML::NodeSet.new(dom.document, slice)
end
def redact_text(text)
text.gsub(/\S+/) { |word| REDACTED_CHARACTER * word.length }
end
end
This makes it a real redaction since a savvy user can’t tweak some CSS to reveal the content.
Paywall component
In my video page layout, I have a component that captures the output of the markdown HTML and stuffs it into a Component::Paywall
.
<article class="prose prose-pre:bg-base-content dark:prose-pre:bg-transparent max-w-full">
<h2><%= video_page.title %></h2>
<%= render Components::Paywall.new(locked: video_page.status.locked?) do %>
<%= yield %>
<% end %>
</article>
The component handles the visual side of the paywall, not to be confused with the first Paywall
class that redacts the content from the HTML.
class Components::Paywall < Components::Base
def initialize(locked: true)
@locked = locked
end
def locked? = @locked
def around_template(&)
if @locked
# Captures and parses the locked and unlocked content.
wall = ::Paywall.new helpers.capture(&)
# Render the unlocked content HTML.
raw wall.preview
# Wrap the preview content in a paywall message.
a(
href: "/phlex#invest",
class: "not-prose block bg-base-100/90 backdrop-blur-md rounded-2xl sticky top-1/2 transform -translate-y-1/2 shadow-xl z-10 m-4 p-4 hover:scale-101 hover:bg-base-100 hover:shadow-2xl transition space-y-2"
) {
h2(class: "text-lg font-bold"){ "🔓 Unlock content"}
p { "Pre-order this course to unlock this video, source code, and content" }
div(class: "btn btn-sm btn-outline") {
plain "Pre-order video course for"
whitespace
span(class: "line-through font-light") { "$379" }
whitespace
plain "$249"
}
}
div(class: "opacity-75") {
# Renders the redacted HTML content.
raw wall.redacted
}
else
yield
end
end
end
I don’t love the design of the UI yet, but it’s functional. I also don’t love how this is implemented in the around_template
method, but I’m not sure how to capture the contents of the block yet. I might take a different approach to this since its a very odd way to implement a Phlex component. Perhaps I’ll pass the current_page
into the Paywall
component as the argument, have the component render it, and make the block the upsell message.
Checkout the paywall in action and consider ordering the course if you want to support this website.