Different assets per subdomain in Rails - ruby-on-rails

So I'm in the process of building a Rails app that will serve different content based on the subdomain.
The app will always be live, and when we want to release a new brand (subdomain), say newbrand.example.com, we will package up assets for that brand using Sprockets and then dump them on S3 (subdomain is the subfolder name eg. /brand/assets/... and /newbrand/assets/...).
Assets and locales will be the only thing that changes per subdomain, I already have a clean solution for the locales (using the i18n-active_record gem) but I'm stumped at different assets per subdomain.
I have some custom code that hooks into Sprockets and packages up an assets dir and creates the necessary manifest file etc:
env = Sprockets::Environment.new
# manifest.compile fails when the below method isn't present
env.context_class.class_eval do
def asset_path(path, options = {})
end
end
%w[config fonts images javascripts stylesheets].each do |folder|
Sprockets.append_path(File.join(Dir.pwd, 'assets', folder))
end
manifest = Sprockets::Manifest.new(env, File.join(Dir.pwd, 'compiled'))
assets = [%w[**/*.jpeg **/*.jpg **/*.svg **/*.png **/*.gif], ['**/*.js'], /(?:\/|\\|\A)application\.(css|js)$/]
manifest.compile(assets)
The idea is that in the background this precompiled assets folder will be sent to S3 via a separate task.
My problem is that I can't get Rails to look for the new assets when visiting on different subdomains.
The way I am switching the asset_host via subdomain is by doing the following:
config.action_controller.asset_host = Proc.new { |source, request|
"http://example.s3.amazonaws.com/#{request.subdomain}/"
}
So when I access newbrand.example.com I want it to load the assets from that subfolder on S3 and so on per brand. Is this possible?

Related

Rails Sprockets file uses local manifest for digests instead of asset host version

In our web application built in Rails we have several clients using the same application who will have different assets that are used dependant on which subdomain is used.
To achieve this we swap out what folder is being used on the CDN like so:
config.action_controller.asset_host = Proc.new { |source, request|
if request.subdomain.present?
"http#{request.ssl? ? 's' : ''}://cdn.domain.com/#{request.subdomain}/"
else
"http#{request.ssl? ? 's' : ''}://#{request.host_with_port}/"
end
}
Each time we create a new client we compile the assets manually using a custom build tool that uses Sprockets to build the assets the same way Rails would and then upload them to our CDN under a folder that matches the subdomain. This then allows us to have different sets of assets based purely on the subdomain.
Now this works fine except that when we update an asset the digest will change for that file but Rails will still try and load the old asset digests because the sprockets-manifest file (which is in /public/assets) e.g. .sprockets-manifest-12345.json is being loaded instead of the one that's on the CDN. Even though the asset host is different it still loads the local one.
Rails it seems doesn't care about other manifest files as the file itself only stores the filename to the fingerprinted version so even when things like the host changes it would normally be able to find the correct asset. It would seem as though Rails has been designed this way deliberately.
However we really need to get Rails to use the manifest file that is on the CDN itself rather than use the one in the public folder local to the application.
After reading the docs, it seems you can change the manifest location. We tried doing it by using the same logic as above for the manifest like so:
config.assets.manifest = Proc.new { |source, request|
if request.subdomain.present?
"http#{request.ssl? ? 's' : ''}://cdn.domain.com/#{request.subdomain}/"
else
"http#{request.ssl? ? 's' : ''}://#{request.host_with_port}/"
end
}
But Rails/Sprockets is still using the local sprockets file... Any ideas why?

How can I have assets compiled into image, stylesheet, and javascript directories?

The asset pipeline puts everything into the same directory. Images, stylesheets, and javascripts all go into /public/assets (although subdirectories are respected).
Is there a way to have them copied into /public/assets/images, public/assets/stylesheets, and public/assets/javascripts?
Adding to the confusion is this line in the rails guide:
http://guides.rubyonrails.org/asset_pipeline.html#coding-links-to-assets
In regular views you can access images in the public/assets/images directory like this:
But rails doesn't use or make a public/assets/images directory.
Solution #1:
Create a new subdirectory within the app/assets/ like all and move the
current asset directories into that the new folder; e.g.
mkdir -p app/assets/all
mv app/assets/{javascripts,images,stylesheets} app/assets/all/
Then when precompiling assets with RAILS_ENV=production rails assets:precompile
it should create those javascripts, images and stylesheets directories
underneath the public/assets directory;
public/assets/javascripts
public/assets/images
public/assets/stylesheets
Solution #2:
You can create a new directory somewhere else in your project; like assets,
instead of app/assets; and add that the new directory to the current Rails
asset paths configuration within the assets.rb initializer
# app/config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.application.root.join("assets")
Then precompiling assets should have the same effect as Solution #1.
Solution #3
So, as part of the Sprocket-Rails Engine it preloads the app/assets
subdirectories with this following block of code:
# ~/ruby/gems/2.3.0/gems/sprockets-rails-3.2.0/lib/sprockets/railtie.rb:54
module Rails
# [...]
class Engine < Railtie
# Skip defining append_assets_path on Rails <= 4.2
unless initializers.find { |init| init.name == :append_assets_path }
initializer :append_assets_path, :group => :all do |app|
app.config.assets.paths.unshift(*paths["vendor/assets"].existent_directories)
app.config.assets.paths.unshift(*paths["lib/assets"].existent_directories)
app.config.assets.paths.unshift(*paths["app/assets"].existent_directories)
end
end
end
end
The line of interest is the
app.config.assets.paths.unshift(*paths["app/assets"].existent_directories)
It call to existent_directories off the *paths["app/assets"] is returning
all the subdirectories within the assets folder, hence why there are no
subdirectories in the public/assets folder when Sprocket computes its digested
assets.
In order to add those sub-directories back in, we have to modify the current
Rails configured environment for Sprockets; i.e.
Rails.application.config.configure. However, the Sprockets::Paths that is
included in the Sprockets::Configuration that
Rails.application.config.configure yields to does not allow public access to
its internal #paths variable, nor does it have an method to remove paths like
it does to add paths via its append_paths. Instead we have to duplicate the
current paths already included by the above Railtie initializer block, remove
the old "app/assets" subdirectories paths that we do not want, add in just the
"app/assets" directory we do want and then append them back into the configured
Sprockets environment; which looks something like this in a Rails initializer:
# app/config/initializers/assets.rb
Rails.application.config.assets.configure do |env|
old_paths = env.paths.dup
new_paths = old_paths - Rails.application.paths["app/assets"].existent_directories
new_paths << Rails.application.root.join("app", "assets")
env.clear_paths
new_paths.each { |path| env.append_path(path) }
end
Closing Comments
Using any of these solution should also mean that you will need to specify the subdirectory in all your
asset_path method calls within your view templates in order to find the
compiled asset; e.g.
<%= asset_path "images/example.png" %>
<%= asset_path "javascripts/application.js" %>
<%= asset_path "stylesheets/application.css" %>
/assets/images/example-ca63e56ac855bdfb187479a35a7476cd65c539727f84fea19e1ad258cf3d23f5.png
/assets/javascripts/application-a4f3e75c7f7aa6d6cbc2ebcbb436b12aca442553219883805baebdd2eecd5471.js
/assets/stylesheets/application-539757f75200b6ea43399bf5e25cbc2819c1e6a610f94d5c9beaf91fec89384e.css
I hope one these solutions help.

Rails asset pipeline doesn't serve images

My Rails app doesn't serve images at all.
image_url('picture.jpg')
# will result in url(http://localhost:3000/images/picture.jpg)
# but should be url(http://localhost:3000/assets/picture.jpg)
image_tag 'picture.jpg'
asset_url 'picture.jpg'
# will result in the same url / path as image_url()
Neither http://localhost:3000/images/picture.jpg nor http://localhost:3000/assets/picture.jpg exists, while http://localhost:3000/assets/images/picture.jpg does.
Here is a gist of my application.rb and development.rb: https://gist.github.com/maximski/1ccb75f6f89c02932239
I am in development environment and I don't want to precompile files manually. The app is pretty much much newly generated so the configuration is almost completely set on default.
This problem appear if images doesn't exists in app/assets/images directory. Check that app/assets/images/picture.jpg file is exists.

How to override public folder path in Rails 4?

I would like to use a different public folder from a parent directory called client which contains the entire AngularJS app. Essentially I want to tell Rails to load AngularJS app and the only job that Rails has to do is serve JSON.
Is that possible in Ruby on Rails?
As others have mentioned, it may or may not be a great idea to override the existing paths['public'] folder. But you can do the following safely in somewhere like application.rb:
Rails.application.config.middleware.insert_after(
ActionDispatch::Static,
ActionDispatch::Static,
Rails.root.join("client").to_s,
Rails.application.config.static_cache_control
)
The public folder is exposed to the web server through the Rack middleware ActionDispatch::Static. There's nothing else special about it, and the above code simply adds another instance of the middleware that points to the directory client. So in the above case, the browser would be able to access everything in public as well as client.
Just had to do it myself for an Angular app.
Put this in your application.rb:
config.serve_static_files = true
paths['public'] = File.join 'client', 'app'
Or if you still use asset pipeline (config.assets.enabled = true):
paths['public/javascripts'] = File.join 'client', 'app', 'scripts'
paths['public/stylesheets'] = File.join 'client', 'app', 'styles'
Would be interesting to know if there are any consequences with the second bit as my front-end is served completely separately thus I keep asset pipeline switched off and use grunt instead.
You can define another path like
# config/application.rb
paths['my_website'] = 'website'
Then you can use this path in your routes like
# routes.rb
get '/my_website', to: redirect('my_website/index.html')

Rails: non matching asset digests

Below is the asset host config of my Rails app
ASSET_HOSTS = ["http://host1.cdn.com", "http://host2.cdn.com"]
config.asset_host = proc do |path, request=nil, *_|
ASSET_HOSTS.sample
end
The problem with this is that one of my javascript files (which is a .js.erb file) has a different digest (when called by javascript_include_tag) than the one specified in manifest.yml (generated during precompilation).
Does anyone know why this happened and how to fix it?

Resources