Zohar Arad

The Blog

SEO Friendly Single-Page Apps with Batman.js and Rails

Getting search engines to crawl your wonderful single-page Website is hard. Since rendering is not done by the server, you have no real way of serving your site content to non-browser clients (like search engine robots).

Some ways of dealing with this challenge include:

  • Running a headless browser that intercepts search-engine requests and mimics the behaviour of a normal (human) user.
  • Duplicating views from your single-page app to the server, or creating separate views and serving them for search engines.
  • Black magic and tons of faith in the wisdom of search engines to run your single-page Javascript code
    I’ve been haunted by this problem for some time and even tried to solve it with a Rubygem called Jader that renders Jade templates using Ruby’s V8 engine. However, running Javascript inside Ruby is slow and frankly not very elegant.

There is, however, a much simpler solution, that requires surprisingly little code, and can probably be adapted to a few of the popular single-page MV* frameworks out there.

Sounds interesting? Well, read on to find out more.

Batman.js and HTML templates

Batman.js is my framework of choice. I won’t list all the reasons here, but there’s one feature Batman.js has, that makes if a very compelling candidate for our SEO-SPA (Single-Page-Application) experiment - HTML-only templates.

Batman’s templates are just HTML with some funky data attributes that contain flow, content and logic. There’s no special template engine (or indeed language) to learn or use. Just pure, simple HTML.

Additionally, Batman’s template usually don’t contain dynamic content. The content is appended by the framework during runtime.

So, a typical Batman template markup will look something like this:

Batman Template
1
<h1 data-bind="post.title"></h1>

`

From HTML to ERB

Since Batman.js works really well with Rails, and Rails’ default template engine, ERB, is just magic tags on top of standard markup, what if we were to take the same markup above, and convert it to ERB for server-side rendering.

ERB Template with Batman data attributes
1
<h1 data-bind="post.title"><%= @post.title %></h1>

Well, what do you know, ERB comes in on top of Batman. Batman’s template engine doesn’t need the ERB part, and ERB doesn’t care about Batman’s data attributes.

So, if we could use ERB on the server, to render our templates for search engine consumption, and use the same template, without ERB for Batman’s consumption, we should have a clear winner.

Setting our goals

Let’s try and clarify what we’re actually trying to do here, and then see how we can build on the example above and fulfil the goals below.

Goal 1 - We want a single-page application that is visible to search engines. Specifically, we aim for the search engine to “see” exactly the same markup and styles as normal users (Google screenshots anyone?).

Goal 2 - We’d like to share our HTML templates between our MV* framework (Batman.js in this case) and the server. Sharing templates serves two purposes - Avoiding code duplication and making sure server-side rendering happens as quickly as possible (meaning we don’t want to resort to non-native or headless-browser based server-side rendering).

We’re going to achieve the above goals, using Rails, ERB and Batman.js with the following steps:

  1. Setup a Rails & Batman.js application
  2. Batman.js
    1. Add HTML templates and suffix them with ERB for server-side rendering
    2. Render templates in controllers from View Cache (see View Cache pre-populating below)
  3. Rails
    1. Add ERB code inside Batman’s templates for server side rendering
    2. Cleanup ERB code in templates and pre-populate them to Batman’s View Cache
    3. Tell Rails to look for render-able templates inside Batman’s template directory instead of the standard app/views directory
    4. Inside Rails controllers, render either ERB template for search engine or JSON for XHR coming from our Batman.js SPA.

The Code

Assuming we have a Rails & Batman.js application installed, here are the important bits that follow the steps above.

First, we add Batman’s template directory to Rails’ view path, so our Batman templates can be rendered by Rails’ controllers.

config/application.rb
1
2
# add Batman template dir to views path
config.paths['app/views'].unshift("#{Rails.root}/app/assets/batman/html")

Next, we add a view helper that will take our Batman, ERB-fuelled templates and clean up and ERB code, reverting them to their “intended”, ERB-less state. The output of this helper will be used to pre-populate Batman’s View Cache.

app/helpers/application_helper.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module ApplicationHelper
# Parse Batman templates, remove ERB tags and return as JSON
# JSON format is {"path/to/template":"template HTML"}
def batman_views_json
prefix = Rails.root.join "app/assets/batman/html"
paths = Dir.glob("#{prefix}/**/*").select{|f| File.file?(f) && (f =~ /\.(html|erb)$/i) }
re = Regexp.new "<%(.*?)%>"
paths.inject({}) do |all_views, f|
viewname = f.sub( /^#{prefix}/, '' ).sub( /\..*$/i, '' )
# this is where we clean our ERB tags
view = File.read(f).gsub(re,'').gsub(/[\n\r]+/,'').gsub(/href=\"\"/,' ')
view = ERB.new(view).result if f =~ /\.erb$/i
all_views[viewname] = view.gsub(/[\r\n\t]+|\s{2}/,'')
all_views
end.to_json
end
end

The code above is a modified version of the code found in a post titled Batman’s Secret Cache).

Now let’s pre-populate Batman’s View Cache with the ERB-less templates (we’re doing this inside our server-side layout, to ensure changes to our HTML templates are always reloaded):

app/views/layout.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
<script type="text/javascript">
(function(){
// get the Batman templates JSON from application helper
var cachedTemplates = <%= raw batman_views_json %>;
for(var view in cachedTemplates){
if(cachedTemplates.hasOwnProperty(view)){
Batman.View.store.set(view, cachedTemplates[view])
}
}
}());
</script>

And finally let’s make sure Rails’ asset pipeline doesn’t try to pre-compile our ERB templates:

config/application.rb
1
2
3
4
config.assets.precompile = [ Proc.new { |path, fn|
fn =~ /app\/assets/ && !%w(.js .css .html).include?(File.extname(path)) },
/application.(css|js)$/
]

To finish things up, let’s look at an example template inside our Batman.js application:

app/assets/batman/html/posts/index.html
1
2
3
4
5
6
7
8
9
10
11
12
<section class="posts">
<%- @posts.each do |post| %>
<article class="post" data-foreach-post="posts">
<header>
<h3 class="post-title">
<a href="<%= post_slug_path(post.id, post.slug) %>" data-route="routes.posts[post]" data-bind="post.title"><%= post.title %></a>
</h3>
</header>
<p data-bind="post.excerpt"><%= post.excerpt %></p>
</article>
<%- end %>
</section>

For client-side, single-page rendering mode, our view helper will read that template, strip all ERB tags and save it to Batman’s View Cache.

For server-side, search-engine mode, Rails will simply read and parse the ERB code, and output it as HTML with all the dynamic content coming from the server (in the above example, a list of blog posts).

A Live Example

Since seeing is believing, check out a live demo of the above technique on Heroku.

If you’re curious to see the client-side version of the ERB templates, simply view the page source and look for a variable called cachedTemplates. If you’re feeling particularly brave, open the Javascript console and type Batman.View.store._htmlContents .

If you want to pretend to be a search engine, visit http://batsoe.herokuapp.com/?se=1 and view the page source.

Closing thoughts

Some of you might ask why Batman and Rails and not Angular.js or Backbone.js . Well, the main reason is Batman’s template engine which relies on HTML tags only and the fact that Batman was designed to work well with Rails (so for example, template naming conventions are the same in Batman as they are in Rails). In short, it made sense that if I chose one, I’d choose the other.

With a bit of work, this technique could be adapted to other Javascript MV* frameworks that have a similar template-engine. Of course, on the server-side, one is not limited to ERB. EJS (or heaven forbid PHP) are also good candidates as they’re HTML based rather than pre-processor based (like HAML and Jade).

The code for the above demo is available on Github. Feel free to fork and spread the word.

Comments

Proudly published with Hexo