46
votes

Note: This question could also read:

How to support bookmarking of hashbang-less client side mvc frameworks in Java.

I am transitioning an angular app that uses hashtags to one that is html5mode. I have successfully set

$locationProvider.html5Mode(true);

And all links from the landing page (index.html) work fine.

The problem is, if partial urls are referenced directly, I get a 404, naturally, since the server endpoint definitions aren't coupled to client side-defined routes.

So without HTML5 we get non-SEO friendly hashbangs, but with it we cannot bookmark anything other than the landing page (the page that bootstraps angular).

Why it works if requesting default landing page (index.html) first, ie htpp://mydomain.com/ :

  1. Browser requests index.html from server
  2. Server returns index.html, and browser loads angular framework
  3. URL changes are sent to the client side router and the proper partial/s are loaded.

Why it doesn't work if (ie) http://mydomain.com/foo is requested directly from the browser:

  1. Browser requests mydomain/foo from server.
  2. Resource doesn't exist
  3. Server returns 404

Something is missing from this story, I just don't know what. Here are the only two answers I can see...

  • It's by design. Is this how it is supposed to work? That users must always land on the client MVC framework's bootstrap page (usually index.html), and then navigate from there. This is not ideal because state cannot be saved and there is no way to bookmark... not to mention crawling.
  • Server solution. Is this worked around with a server side trick? For example, on all requests, return index.html and immediately call router with additional context. If so, this is against the objective that AngularJS is completely client-side and seems like a hack.
3
You should specify the url base in the head of your main html file (<base href="/my-base">)dimirc
The last bullet point is the solution. Implement some kind of filter on the server-side that serves index.html for all the application URLs. Nothing more is needed. The router will automatically display the page associated with the URL in the location bar.JB Nizet
Seems like that should be included in the AngularJS documentation. Something like "oh yeah, and when you turn on html5mode you also need to make a server side change to make sure that all requests containing your base uri return the bootstrapping html (ie index.html) resource."Robert Christian
I agree with you. The problem is that angular is a pure client-side framework, and that the way to to the server-side configuration depends on the technology used at server-side, which angular doesn't care about. My guess is that they found it obvious enough not to be mentioned. Given the complexity of angular, I would expect it to be used by experimented developers knowing what must be done at server-side.JB Nizet
I found what I believe is the best solution, and as JB says, it's my second bullet above. It turns out that Angular does mention the server side support requirement in their docs: "Using (html5) mode requires URL rewriting on server side, basically you have to rewrite all your links to entry point of your application (e.g. index.html)"Robert Christian

3 Answers

40
votes

The AngularJS documentation does in fact mention this

Server side Using this mode requires URL rewriting on server side, basically you have to rewrite all your links to entry point of your application (e.g. index.html)

In this case, one Java-based solution is to tell the server "map all urls to index.html." This can be done in any HTTP Server or container. I implemented this using Java/Servet since I want my application to be HTTP server agnostic (ie Apache versus NginX, or Tomcat/JBoss only).

In web.xml:

  <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>

  <servlet>
      <servlet-name>StaticServlet</servlet-name>
      <jsp-file>/index.jsp</jsp-file>
  </servlet>

  <servlet-mapping>
      <servlet-name>StaticServlet</servlet-name>
      <url-pattern>/app</url-pattern>
  </servlet-mapping>

And index.jsp simply looks like:

<%@ include file="index.html" %>

And add the following to the tag within index.html:

<base href="/app" />
3
votes

I spent some time thinking about this for my PHP site. I hang all my server side code off the /api route to keep it separate from Angular. Here is the solution I came up with by updating my apache config:

RewriteEngine on
#let the php framework do its thing
RewriteRule ^(api/.*)$ index.php?url=$1 [QSA,L,NC]
#let angular do its thing
RewriteCond %{REQUEST_FILENAME} !-f      
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*) index.html [NC,L]

I wrote up how I went about this using the Flight PHP framework at http://www.awnage.com/2013/10/10/taking-flight-with-angularjs/

-3
votes

The URLs that you link to, and place in the user's address bar, should refer to valid content on the server, and if at all possible, they should refer to the correct content. Sure, you can just serve the same page for every URL, and have the client code go off and load the real content, and things function pretty much the way they do in the hash-URL world. In fact, that's the least-code way to go. But it also qualifies as "breaking the internet", which is why HTML5 and History API give you a way to link to semantically correct URLs.

As a small example, If you go to https://github.com/kriskowal/q and you click on "examples", the client-side code will load the "examples" direcory into the file browser without leaving the page, and the URL bar will read https://github.com/kriskowal/q/tree/master/examples. If you go to https://github.com/kriskowal/q/tree/master/examples directly, you get the content of the "examples" directory sent directly to your browser, without client-side code intermediating. Yes, this is harder to do, and maybe impossible in a "single page app", but it's the right thing to do any time it's possible.