Rails 3 Engines/Plugins and Static Assets

THIS INFORMATION IS OUT OF DATE!  Since Rails 3.2, you can easily include static assets in your engine using the “app/assets” folder.  This post is only applicable for older versions of Rails, and still not suggested for production systems.

Engine-ish

One of the big changes in Rails 3 is the move to “everything is an engine”.  You end up reading a lot about this in relation to plugins, mentioning how this architecture makes it so you can easily embed entire rails applications into others since they’re all just derivatives of Railties.  While this holds true for very simple cases, you’ll quickly find simply creating a rails app, and sticking it in vendor/plugins doesn’t work the way you’d expect from reading about it.  You really do need to create your plugin in a certain way to actually make it work.  Over here is a good writeup on creating a Rails Engine based plugin installed as a gem.

Serving Static Assets

Now, serving static assets from your plugin is a bit on the unintuitive side, and likely for a good reason.  You generally do not want to do this for any kind of production system. However, like most rules, there are times when breaking them makes pragmatic sense.  In this use case we have an internal tool that we want to be able to easily extend.  The maximum number of concurrent users will likely be in low teens, and serving static assets through the rails app is a non-issue.  Extending the app includes providing views, controllers, models, routes, and images.  The first three are extremely straightforward and intuitive, routes use a slightly different enclosing syntax in an Engine, but are otherwise identical.  Images, however are a bit tricky.

There are two ways to create an Engine based plugin, and they are a bit incompatible with each other.  You can either install your plugin as a gem, or as a plugin in the app.  In both cases, you are going to use the ActionDispatch::Static middleware to serve your content. I am also assuming that you are placing your content in the “public” directory in the root of your plugin’s file structure.

Gem Based Plugin

A gem based plugin is initialized via the definition of itself in your plugin’s lib directory.  You can see an example of this below.  In order to set it up correctly you should put your middleware line in an initializer in your declaration:

module MyEngine
  class Engine < Rails::Engine
    initializer "static assets" do |app|
      app.middleware.use ::ActionDispatch::Static, "#{root}/public"
    end
  end
end

In App Plugin

Unfortunately, if the above file exists in a plugin installed to vendor/plugins your app will fail to load.  This is because rails automatically assumes all plugins are Engines, and initializes them as instances of the Plugin class which inherits from Engine.  It will autoload everything in lib, see the declaration, and then fail out with the incredible error message “[Your Plugin] is a Railtie/Engine and cannot be installed as plugin”. Which is somewhat misleading since a plugin is an Engine, just not explicitly.

To get around this issue, you just have to move where you do your initialization and setup. Inside your plugin’s init.rb (which should be in the root of your plugin’s file structure) is where you can do this kind of stuff.  Rails boots your Plugin, and then loads this file with the variable “config” set for you to do your work.  So it’s simply:

config.middleware.use ::ActionDispatch::Static, "#{root}/public"

Load Order

I strongly recommend using middleware.use as it will place your middleware after the parent app, and therefore will cause your plugins assets to be of lower priority than everything else in the stack. This means that files in the parent app’s public directory will load instead of the plugin’s in the case of a conflicting name.  You still have options though.  To give your plugin’s static files priority over everything except the parent app’s static files, use:

middleware.insert_after ::ActionDispatch::Static, ::ActionDispatch::Static, "#{root}/public"

To give your plugin top priority in static asset serving use:

middleware.insert_before ::ActionDispatch::Static, ::ActionDispatch::Static, "#{root}/public"

Leave a Reply

Your email address will not be published. Required fields are marked *

15 Comments

  1. Tweets that mention Rails 3 Engines/Plugins and Static Assets « Technical Ramblings -- Topsy.com

    […] This post was mentioned on Twitter by stonean, Jon Swope. Jon Swope said: Serving static assets from a Rails 3 Plugin (also a little bit of complaining about what a Rails Engine really is): http://swo.pe/9 […]

  2. Steven Heidel

    This works great in Ruby 1.8.7, but won’t work in Ruby 1.9.2.p0. Anyone else had any problems with 1.9.2?

  3. Jon Swope

    I tried it out using the in-app plugin with 1.9.2, and didn’t run into an issue. I’ll try again with a gem based plugin later. Can you give me any more information about what isn’t working, and how you were implementing it?

  4. Steven Heidel

    Whoops, looks like it was a caching problem unrelated to this. Works great now!

    btw I was working on the Refinery CMS project, which is so much more organized now that we have public folders in engines.

  5. Keith Schacht » Blog Archive » Creating a rails 3 engine / plugin / gem – Keith Schacht’s blog

    […] Modest Rubyist, Jon Swope, Rails Dispatch Filed under: GeekKeith Get notified of new […]

  6. Keith Schacht

    Thanks for your help Jon! I’ve incorporated Jon’s tips and many other tips for rails engines into a nice engine starting point:

    http://keithschacht.com/creating-a-rails-3-engine-plugin-gem

  7. Roger

    I wanna use this in production mode but when I do, the stylesheet files are empty when loaded. Any ideas?

  8. Jason L Perry

    Roger, I’m having the same issue. Did you find a solution?

  9. Jon Swope

    I think this is due to the way that Rails compiles the stylesheets together for prod. This technique is for serving static assets from your app, however the method in which it works doesn’t cause your main app to suddenly become “aware” of your plugin’s public directories. All it does is add itself to the middleware stack to intercept requests for assets as they come in.

    I’ll take a look into the code a bit deeper and see if there’s some way to do what you are looking for, but I’m not extremely hopeful as there was no way to handle this without the middleware.

  10. Hussein Morsy

    In production mode all assets (stylesheets, images, javascripts) from the engines are empty. Any solution ?

  11. cowboycoded.com

    Thanks Jon. I found this to be helpful in including some static assets from my engine in my main rails app, but I wanted to append these assets to my main rails app public dir, and not replace the public directory altogether. I ended up using the engine initializer, but I just created symlinks to the files from the plugin directory to the app directory:

    initializer “static assets” do |app|
    system(“ln -nfs #{root}/public/stylesheets/* #{app.root}/public/stylesheets/”)
    system(“ln -nfs #{root}/public/javascripts/* #{app.root}/public/javascripts/”)
    end

  12. Bala Paranj

    Symlinks is a workaround and not a ideal solution. Ideally you want a Rack app to handle all the static assets (html, css, js, images etc). Any pointers on how to do that?

  13. Les Nightingill

    Just to add info to the problems that others (and I) are seeing with blank asset files being delivered…
    The engine’s asset files ARE being found (if I try to include a non-existent css file, I get http not found response, but the blank files include http 200, OK, code). But the content is empty!
    (OK locally on my dev machine, though).

  14. Les Nightingill

    Well I kinda solved the problem with blank assets, with a bit of hacking. Not sure if there will be side effects but would be interested if others can use this too.

    I moved my ::ActionDispatch::Static middleware up in the rack stack. Instead of app.middleware.use (as Jon has suggested, above) I inserted my engine’s static-serving middleware near the top of the rack stack with
    app.middleware.insert_before ::Rack::Lock, ::ActionDispatch::Static, “#{root}/public”
    and presto!

  15. Nicholas Hughes

    @Les

    Thanks!

    I used your suggestion to mount a Rails 3.1 engine in stage/production. I had to change “#{root}/public” to “#{root}/app/assets” as the engine’s assets are in accordance with Rails’ new asset locations.

    initializer “static assets” do |app|
    app.middleware.insert_before ::Rack::Lock, ::ActionDispatch::Static, “#{root}/app/assets”
    end

    I imagine there is a more elegant way to do this, but I could not uncover it today. I hope this helps someone!