04 February 2014
When using Angular's HTML5 location mode you need to find a way to serve index.html
even when the GET request hits a deep link. e.g. a direct request to /products/35
needs to return the contents of index.html
in order to fire up your angular app. Perhaps my google-fu was weak yesterday but I couldn't find any great examples of doing this when using yeoman and rails.
There are a few things to deal with here, the first is adding the rewriting.
I'm using connect-modrewrite for the url rewriting, lets install it first, in your angular app's directory:
$ npm install connect-modrewrite --save
Then in your Gruntfile, add the modrewrite middleware. You may or may not have a middleware section already, I've included a snipit that has the full middleware function in case you don't have one. It also includes a proxy for api calls to the rails server. The modrewrite bit is line 19.
1 livereload: {
2 options: {
3 open: true,
4 base: [
5 '.tmp',
6 '<%= yeoman.app %>'
7 ],
8 middleware: function (connect, options) {
9 var middlewares = [];
10 var directory = options.directory || options.base[options.base.length - 1];
11 if (!Array.isArray(options.base)) {
12 options.base = [options.base];
13 }
14
15 // Setup the proxy to the rails backend for api calls
16 middlewares.push(require('grunt-connect-proxy/lib/utils').proxyRequest);
17
18 //enable modrewrite for html5mode
19 middlewares.push(require('connect-modrewrite')(['^[^\\.]*$ /index.html [L]']));
20
21 options.base.forEach(function(base) {
22 // Serve static files.
23 middlewares.push(connect.static(base));
24 });
25
26 // Make directory browse-able.
27 middlewares.push(connect.directory(directory));
28
29 return middlewares;
30 }
31 }
32 },
This tells the grunt server to rewrite any url that doesn't contain a .
to request /index.html
.
The theory here is that all your static assets: css, js, images, fonts, etc contain .
, your deep routes shouldn't. e.g. /products/54/tags
has no .
but /scripts/controllers/products.js
does have a .
.
It's also important that your proxy is before the rewrite in the middleware stack as your api urls probably don't contain .
and would be rewritten if the proxy were after the rewrite.
There are a few other things that have to be fixed. Yeoman's default configuration for bower-install
and usemin
produce link
and script
tags with relative paths for href
.
For example, take a look at your index.html
file
<!-- build:css styles/vendor.css -->
<!-- bower:css -->
<link rel="stylesheet" href="bower_components/select2/select2.css" />
<link rel="stylesheet" href="bower_components/angular-loading-bar/src/loading-bar.css" />
<!-- endbower -->
<!-- endbuild -->
bower-install
is creating these link tags for you and usemin will process them into something like:
<link rel="stylesheet" href="styles/vendor.css">
The problem here is that the links are relative, when index.html
is served from the root of the server, that isn't a problem. But, when index.html
is served from a deep link, the relative path is wrong.
GET /products/35
<- returns index.html contents
href="styles/vendor.css"
now tries GET /products/35/styles/vendor.css
To fix this you need to make the path absolute, which in this example just means adding a /
in front of the urls.
For bower-install
open up your Gruntfile and find this section:
'bower-install': {
app: {
html: '<%= yeoman.app %>/index.html',
ignorePath: '<%= yeoman.app %>/'
}
},
And remove the trailing /
from the ignorePath
:
'bower-install': {
app: {
html: '<%= yeoman.app %>/index.html',
ignorePath: '<%= yeoman.app %>'
}
},
For usemin, find all the usemin build comments in your index.html
file and add a leading /
to the output file name, for example:
<!-- build:css styles/vendor.css -->
becomes <!-- build:css /styles/vendor.css -->
You should also add leading slashes to anything you've added to index.html
like your controllers, services, and directives scripts.
Finally, you need to add leading slashes to the es5-shim.js
and json3.min.js
conditional imports yeoman generates.
This assumes your using yeoman to build your files into the rails public directory such that rails is serving them. I take this approach for simple deployment of the full app to Heroku, you can setup a CDN in front of these assets for production use.
Since you can't do url rewriting in front of the rails app with heroku, your stuck doing it somewhere in the rails stack. I'm using rake-rewrite for this.
Add gem 'rake-rewrite'
to your Gemfile and bundle install
.
In Application.rb
add the middleware, depending on what your Application.rb
already looks like, it'll end up something like this:
module YourApp
class Application < Rails::Application
config.serve_static_assets = true
config.middleware.insert_before ActionDispatch::Static, Rack::Rewrite do
rewrite %r{^(?!/api/).*}, '/index.html', :not => %r{(.*\..*)}
end
end
end
The middleware is being inserted before the static file server and the rewrite rule is says: If the url doesn't start with '/api/' rewrite to '/index.html' unless the url contains a .
.
You may have to tweak this a bit for your environment, I serve my api endpoints from '/api/v{x}' so I'm excluding anything that starts with '/api/' from the rewrite. I'm also using the same .
logic used in the grunt rewriting.
Hope this helps!
keys punched by Mark Bell