Making HTML emails comfortable for the Ruby rockstars
Making HTML emails comfortable for the Ruby rockstars
[][passive]
| :warning: | This gem is now in [passive maintenance mode][passive]. [(more)][passive] |
Making HTML emails comfortable for the Ruby rockstars
Roadie tries to make sending HTML emails a little less painful by inlining stylesheets and rewriting relative URLs for you inside your emails.
Email clients have bad support for stylesheets, and some of them blocks stylesheets from downloading. The easiest way to handle this is to work with inline styles (style="..."), but that is error prone and hard to work with as you cannot use classes and/or reuse styling over your HTML.
This gem makes this easier by automatically inlining stylesheets into the document. You give Roadie your CSS, or let it find it by itself from the <link> and <style> tags in the markup, and it will go through all of the selectors assigning the styles to the matching elements. Careful attention has been put into selectors being applied in the correct order, so it should behave just like in the browser.
"Dynamic" selectors (:hover, :visited, :focus, etc.), or selectors not understood by Nokogiri will be inlined into a single <style> element for those email clients that support it. This changes specificity a great deal for these rules, so it might not work 100% out of the box. (See more about this below)
Roadie also rewrites all relative URLs in the email to an absolute counterpart,
making images you insert and those referenced in your stylesheets work. No more
headaches about how to write the stylesheets while still having them work with
emails from your acceptance environments. You can disable this on specific
elements using a data-roadie-ignore marker.
!important styles.style attribute of tags.:hover, @media { ... } and friends around in a separate <style> element.hrefs and img srcs absolute.data-roadie-ignore markers before finishing the HTML.Add this gem to your Gemfile as recommended by Rubygems and run bundle install.
gem 'roadie', '~> 5.2'You can then create a new instance of a Roadie document:
# Transform full documents with the #transform method.
document = Roadie::Document.new "<html><body></body></html>"
document.add_css "body { color: green; }"
document.transform
# => "<html><body style=\"color:green;\"></body></html>"
# Transform partial documents with #transform_partial.
document = Roadie::Document.new "
"
document.add_css "div { color: green; }"
document.transform_partial
# => "
"Your document instance can be configured with several options:
url_options - Dictates how absolute URLs should be built.keep_uninlinable_css - Set to false to skip CSS that cannot be inlined.merge_media_queries - Set to false to not group media queries. Some users might prefer to not group rules within media queries because
it will result in rules getting reordered.
e.g.@media(max-width: 600px) { .col-6 { display: block; } }
@media(max-width: 400px) { .col-12 { display: inline-block; } }
@media(max-width: 600px) { .col-12 { display: block; } }@media(max-width: 600px) { .col-6 { display: block; } .col-12 { display: block; } }
@media(max-width: 400px) { .col-12 { display: inline-block; } }asset_providers - A list of asset providers that are invoked when CSS files are referenced. See below.external_asset_providers - A list of asset providers that are invoked when absolute CSS URLs are referenced. See below.before_transformation - A callback run before transformation starts.after_transformation - A callback run after transformation is completed.serialization_options - An integer bitmap of options passed along to Nokogiri during serialization.
By default it's Nokogiri::XML::Node::SaveOptions::NO_DECLARATION | Nokogiri::XML::Node::SaveOptions::NO_EMPTY_TAGS.In order to make URLs absolute you need to first configure the URL options of the document.
html = '... <a href="/about-us">Read more!</a> ...'
document = Roadie::Document.new html
document.url_options = {host: "myapp.com", protocol: "https"}
document.transform
# => "... <a href=\"https://myapp.com/about-us\">Read more!</a> ..."The following URLs will be rewritten for you:
a[href] (HTML)img[src] (HTML)url() (CSS)You can disable individual elements by adding an data-roadie-ignore marker on
them. CSS will still be inlined on those elements, but URLs will not be
rewritten.
<a href="|UNSUBSCRIBE_URL|" data-roadie-ignore>Unsubscribe</a>By default, style and link elements in the email document's head are processed along with the stylesheets and removed from the head.
You can set a special data-roadie-ignore attribute on style and link tags that you want to ignore (the attribute will be removed, however). This is the place to put things like :hover selectors that you want to have for email clients allowing them.
Style and link elements with media="print" are also ignored.
<head>
<link rel="stylesheet" type="text/css" href="/assets/emails/rock.css">
<link rel="stylesheet" type="text/css" href="http://www.metal.org/metal.css">
<link rel="stylesheet" type="text/css" href="/assets/jazz.css" media="print">
<link rel="stylesheet" type="text/css" href="/ambient.css" data-roadie-ignore>
<style></style>
<style data-roadie-ignore></style>
</head>Roadie will use the given asset providers to look for the actual CSS that is referenced. If you don't change the default, it will use the Roadie::FilesystemProvider which looks for stylesheets on the filesystem, relative to the current working directory.
Example:
# /home/user/foo/stylesheets/primary.css
body { color: green; }
# /home/user/foo/script.rb
html = <<-HTML
<html>
<head>
<link rel="stylesheet" type="text/css" href="/stylesheets/primary.css">
</head>
<body>
</body>
</html>
HTML
Dir.pwd # => "/home/user/foo"
document = Roadie::Document.new html
document.transform # =>
# <!DOCTYPE html>
# <html>
# <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>
# <body style="color:green;"></body>
# </html>If a referenced stylesheet cannot be found, the #transform method will raise an Roadie::CssNotFound error. If you instead want to ignore missing stylesheets, you can use the NullProvider.
You can write your own providers if you need very specific behavior for your app, or you can use the built-in providers. Providers come in two groups: normal and external. Normal providers handle paths without host information (/style/foo.css) while external providers handle URLs with host information (//example.com/foo.css, localhost:3001/bar.css, and so on).
The default configuration is to not have any external providers configured, which will cause those referenced stylesheets to be ignored. Adding one or more providers for external assets causes all of them to be searched and inlined, so if you only want this to happen to specific stylesheets you need to add ignore markers to every other styleshheet (see above).
Included providers:
FilesystemProvider – Looks for files on the filesystem, relative to the given directory unless otherwise specified.ProviderList – Wraps a list of other providers and searches them in order. The asset_providers setting is an instance of this. It behaves a lot like an array, so you can push, pop, shift and unshift to it.NullProvider – Does not actually provide anything, it always finds empty stylesheets. Use this in tests or if you want to ignore stylesheets that cannot be found by your other providers (or if you want to force the other providers to never run).NetHttpProvider – Downloads stylesheets using Net::HTTP. Can be given a whitelist of hosts to download from.CachedProvider – Wraps another provider (or ProviderList) and caches responses inside the provided cache store.PathRewriterProvider – Rewrites the passed path and then passes it on to another provider (or ProviderList).If you want to search several locations on the filesystem, you can declare that:
document.asset_providers = [
Roadie::FilesystemProvider.new(App.root.join("resources", "stylesheets")),
Roadie::FilesystemProvider.new(App.root.join("system", "uploads", "stylesheets")),
]NullProviderIf you want to ignore stylesheets that cannot be found instead of crashing, push the NullProvider to the end:
# Don't crash on missing assets
document.asset_providers << Roadie::NullProvider.new
# Don't download assets in tests
document.external_asset_providers.unshift Roadie::NullProvider.newNote: This will cause the referenced stylesheet to be removed from the source code, so email client will never see it either.
NetHttpProviderThe NetHttpProvider will download the URLs that is is given using Ruby's standard Net::HTTP library.
You can give it a whitelist of hosts that downloads are allowed from:
document.external_asset_providers << Roadie::NetHttpProvider.new(
whitelist: ["myapp.com", "assets.myapp.com", "cdn.cdnnetwork.co.jp"],
)
document.external_asset_providers << Roadie::NetHttpProvider.new # Allows every hostCachedProviderYou might want to cache providers from working several times. If you are sending several emails quickly from the same process, this might also save a lot of time on parsing the stylesheets if you use in-memory storage such as a hash.
You can wrap any other kind of providers with it, even a ProviderList:
document.external_asset_providers = Roadie::CachedProvider.new(document.external_asset_providers, my_cache)If you don't pass a cache backend, it will use a normal Hash. The cache store must follow this protocol:
my_cache["key"] = some_stylesheet_instance # => #<Roadie::Stylesheet instance>
my_cache["key"] # => #<Roadie::Stylesheet instance>
my_cache["missing"] # => nilWarning: The default Hash store will never be cleared, so make sure you don't allow the number of unique asset paths to grow too large in a single run. This is especially important if you run Roadie in a daemon that accepts arbritary documents, and/or if you use hash digests in your filenames. Making a new instance of CachedProvider will use a new Hash instance.
You can implement your own custom cache store by implementing the [] and []= methods.
…If you are using Rspec, you can test your implementation by using the shared examples for the "roadie cache store" role:
require "roadie/rspec"
describe MyRoadieMemcacheStore do
let(:memcache_client) { MemcacheClient.instance }
subject { MyRoadieMemcacheStore.new(memcache_client) }
it_behaves_like "roadie cache store" do
before { memcache_client.clear }
end
endPathRewriterProviderWith this provider, you can r
No open issues yet, or sync has not completed.