10

I'm building a website using AngularJS and Rails. The HTML files that I'm using for templates are stored under /app/assets/templates and each time I update a route or change something inside of a nested partial inside of a template I need to "touch" the highest level file in the /app/assets/templates directory for the html file I'm changing.

So if I have a page "edit.html" which loads a partial "_form.html" then whenever I update a route or change something in _form.html I need to make sure that edit.html is touched.

This is annoying and very finicky. Is there any way to inform the asset pipeline/sprockets to avoid caching for the app/assets/templates directory?

matsko
  • 21,895
  • 21
  • 102
  • 144

6 Answers6

21

The best solution I've found to this is not to use the asset pipeline for HTML template files.

Instead make a controller called TemplatesController and create only one action. Then map all template URLs to that using a route such as:

get /templates/:path.html => 'templates#page', :constraints => { :path => /.+/  }

Then move all the template files into app/views/templates

Then inside the controller, setup the following:

caches_page :page

def page
  @path = params[:path]
  render :template => 'templates/' + @path, :layout => nil
end

This way all of your template files will be served from the controller and then will be cached into public/templates. To avoid cache problems, you can create a timestamp path into the template route so that your cached files are delivered with a version:

get '/templates/:timestamp/:path.html' => 'templates#page', :constraints => { :path => /.+/ }

This way you can have a new timestamp each time you upload the website and you can store the templates folder anywhere you like. You can even store the templates folder on S3 and have an assets URL for that. Then wherever your template files are addressed, you can use a custom asset method:

templateUrl : <%= custom_asset_template_url('some/file.html') %>

Where:

def custom_asset_template_url(path)
  "http://custom-asset-server.website.com/templates/#{$some_global_timestamp}/#{path}"
end

Then just make the asset redirect to the Rails server if it's not found and it will be generated. Or all template files can be pre-generated once uploaded.

matsko
  • 21,895
  • 21
  • 102
  • 144
  • 3
    That solution is ok, because it works. However it doesn't work as good as for other assets - it lacks the caching with fingerprinting. – mateusz.fiolka Nov 01 '12 at 17:11
  • 1
    The major tradeoff is that you if you continue using the assets directory to deliver template files then you do not have access to `partials`, `routes`, `url helpers` and `any other shared Rails code`. You also have to manually "touch" the HTML files each time you change them. While it's nice to have fingerprinted files within your assets, the problem then becomes having to upload those asset files each time you deploy. You will also need to create a CORS policy for your asset files since it's likely that your templates will be delivered by a CDN. You can see why I chose to do it this way. – matsko Nov 01 '12 at 19:23
  • This solution is really great! There is an error though: you define a route to the action file, but in the controller you are using the action page. – yagooar Apr 18 '13 at 18:39
  • Great suggestion! I'll be using this! Thanks! – bbonamin May 21 '13 at 12:59
  • Really great approach, this one. It really spared me hours of frustration from dinking around with the Rails asset pipeline and a CDN – yalestar Feb 28 '14 at 03:38
  • Is this still the best option in 2014, or would it be better to use something as https://github.com/pitr/angular-rails-templates? – Dofs Aug 03 '14 at 18:56
  • This is the best option if your app does not use templates on all pages. The angular-rails-templates gem loads the templates as assets and those will be downloaded on every page. Just one note for the solution above, it is best to place the timestemp on redis or similar so multiple servers could read it. This way this method will be supported on a multi instance environment. – bymannan Feb 01 '15 at 07:50
  • This approach would be absurdly slow on a high-scale production server, as Rails is awful at rendering static files (hence asset pipeline) – OneChillDude Sep 16 '15 at 21:12
6

There's a much (much!) better way to deal with this.

<%= path_to_asset("template_name.html") %>

That will return a fully working file from the asset pipeline, which can use ERB, etc. It's undocumented, but it's a part of sprockets / the asset pipeline.

RandallB
  • 5,415
  • 6
  • 34
  • 47
  • Nope. I haven't needed that before. – RandallB Mar 17 '13 at 01:16
  • 1
    Therein lies the problem. AWS does not support SSL AND CORS on CloudFront or S3, so you either need to pay for a dedicated CDN like Limelight or serve your own HTML like @matsko suggests. That is if you want to avoid caching problems. – smothers Mar 25 '13 at 21:21
  • S3 does provide CORS header support inside of the S3 manager. Is this not fully functional though? – matsko May 21 '13 at 20:46
  • Cloudfront does support SSL for some ghastly price like $300 / month. For S3: http://docs.aws.amazon.com/AmazonS3/latest/dev/cors-troubleshooting.html – RandallB Jun 22 '14 at 23:25
3

To my mind, several things are needed here:

  1. The templates should be namespaced, so people templates go in the app/views/people/templates directory
  2. The templates are entirely static, so no before filters should be called.
  3. The templates should be cached, making them very fast.

Here's a possible solution using a Rails concern:

# Allows static content to be served from the templates
# directory of a controller
module HasTemplates

  extend ActiveSupport::Concern

  included do
    # Prepend the filter
    prepend_before_filter :template_filter, only: [:templates]
    # Let's cache the action
    caches_action :templates, :cache_path => Proc.new {|c| c.request.url }
  end

  # required to prevent route from baulking
  def templates;end

  # Catch all template requests and handle before any filters
  def template_filter
    render "/#{params[:controller]}/templates/#{params[:template]}", layout: 'blank'
    rescue ActionView::MissingTemplate
      not_found layout: 'blank'
    false
  end
end

Notice we are returning the template in a prepended filter. This allows us to return the static content without hitting any other filters.

You can then create a route, something like this:

resources :people do
  collection do
    get 'templates/:template' => 'people#templates', as: :templates
  end
end

Your controller becomes simply:

class PeopleController < ApplicationController
  include HasTemplates
end

Now any file placed in the /app/views/people/templates can be served at speed from a url.

superluminary
  • 47,086
  • 25
  • 151
  • 148
  • This is a great approach, can you describe how a JS front end framework would access that info? I don't like the idea of having to async load a template file for each JS view. Or is there a way to setup a layout with surrounding script tags and include them at the bottom of the main layout page? – aviemet Nov 02 '15 at 22:49
1

Expanding on RandallB's answer a bit; this is mentioned explicitly in the documentation on the Asset Pipeline: http://guides.rubyonrails.org/asset_pipeline.html

Note that you have to append the extension .erb to your .coffee file to have this work. (e.g., application.js.coffee.erb)

dzuc
  • 761
  • 8
  • 12
1

You can try gem js_assets (https://github.com/kavkaz/js_assets).

This allows you to function asset_path in javascript code that emulates a similar method of sprockets.

kavkaz
  • 46
  • 3
0

Cleanest solution is to precompile your html assets using ejs and serve them as other javascript files.

  • No http query
  • minification
  • Possibility to pass js data to you templates to make them dynamic (ejs is a port from underscore template and basically behave like erb for javascript)

It basically work like that :

    #in you gemfile
    gem 'ejs'

    #in app/assets/javascripts/templates/my_template.jst.ejs
    <p>my name is <%= name %> !<p>

    #in your application.coffee
    #= require_tree ./templates

    JST['templates/my_template'](name: 'itkin')
    => '<p>my name is itkin !<p>'
nicolas
  • 900
  • 6
  • 16