February 24th, 2015 by Matyas Danter

SEO-friendly AngularJS with HTML5 pushState(), Rewrite, and twelve lines of code

ng_logo

While migrating an e-commerce application (piqchocolates.com) from Grails and Tomcat to an AngularJS, Java EE (JAX-RS), and JBoss WildFly stack, I had to make sure that the new platform has feature parity in all areas that are valuable to our business. Search Engine Optimization (SEO) is crucial for us because we primarily market our business on-line. In short, we need search engine optimized URLs, and deep linking; this article will show you how to implement both.

Issues with SEO

My initial research revealed SEO issues with AngularJS fragments and hash bang (“#!”) URLs. Namely, web crawlers don’t know how to follow links properly when applications use this URL scheme.

The work-arounds were cumbersome, so it was time to turn to HTML5 “pushState”, a new browser feature that enables the URL to change without sending full page requests back to the server; “pushState” is painless to enable in an AngularJS application.

Configure the client

var app = angular.module('app', ['ngRoute']);

// Enable html push state
app.config(function($locationProvider) {
  $locationProvider.html5Mode({ enabled: true, requireBase: true });	
});
and
<!doctype html>
<html class="no-js" lang="en">
<head>
    <base href="/"> <!-- this is important -->
</head>
    <body>
    	<script src="libs/angular/angular.js"></script>
    	<script src="scripts/app.js"></script>
    </body>
</html>

The code above ensures that as you navigate your SPA via links and buttons, the browser’s address bar is updated accordingly to represent the new state within the application.

In terms of business value, the more important feature is allowing state to survive through page refreshes. This is where SEO comes into the picture, and with that, Rewrite.

Configure the server

To use Rewrite in a JAX-RS and AngularJS application, simply include this dependency in your project’s `pom.xml` dependencies.

rewrite dependency
<dependency>
   <groupId>org.ocpsoft.rewrite</groupId>
   <artifactId>rewrite-servlet</artifactId>
   <version>2.0.12.Final</version> <!-- or latest version -->
</dependency>

Once you’ve done that. Simply create a Rewrite configuration similar to the following; this redirects all requests (that are not mapped to existing directories or Servlets) to `/index.html`. (This assumes that `/index.html` is the access point to your Angular application.

PushStateConfigurationProvider.javaView the entire file
@RewriteConfiguration
public class PushStateConfigurationProvider extends HttpConfigurationProvider
{
    @Override
    public Configuration getConfiguration(final ServletContext context)
    {
        return ConfigurationBuilder.begin()
            .addRule()
            .when(Direction.isInbound().and(Path.matches("/{path}"))
                .andNot(Resource.exists("/{path}"))
                .andNot(ServletMapping.includes("/{path}")))
            .perform((Log.message(Level.INFO, "Forwarding to index.html from {path}").and(Forward.to("/index.html"))))
            .where("path").matches(".*");
    }

    @Override
    public int priority()
    {
        /* This provides ordering if you have multiple configurations */
        return 10; 
    } 
}

The resulting setup is two lines of code in AngularJS, another ten lines of code for the URL Rewrite Configuration, enables us to have all the configuration in one place, and even includes meaningful and easily configurable logging.

Most importantly, since the site is now web crawler friendly, our application now supports Search Engine Optimization. Additionally, if that’s not all, this configuration is actually incredibly simple, and readable. It should be no trouble to understand what this does when I come back to revisit this code for our next upgrade. Rewrite FTW!

Conclusion

I hope this is helpful to you, and that it reinforces that Java and JavaScript are a combination to be reckoned with.

About Piq Chocolates

Piq Chocolates provides personalized and custom shaped chocolates for gifts, favors, events.


Matyas Danter is a Senior Consultant at Red Hat Software. He is interested in cryptography, software development awesomeness, and enterprise web applications.

Posted in AngularJS, Best Practices, Java, JavaScript, JBoss, OpenSource, Rewrite

9 Comments

  1. Bill Bensing says:

    Thank you very much for this, this is awesome!

    I’m having to write a servlet for my angularjs app because it is hosted on the Goolge AppEngine. How does the above code play into the bigger picture? What calls the PushStateConfigurationProvider.java when the application is loaded? Also, what are the dependencies for Path, Resource, ServletMapping, Log and Forward?

    Sorry for the rudamentary questions, I’m backing into learning servlets.

    1. Hi Bill,

      You’re welcome.

      The big picture: Suppose that your app contains a url http://fqdn/foo/bar/baz. Because we are using HTML5 pushState() the URI /foo/bar/baz is not a physical resource (directory, image, etc.) on the server side, but rather AngularJS route information. When the server side obtains a request for http://fqdn/foo/bar/baz this request is forwarded to index.html, which is where AngularJS is bootstrapped. Once AngularJS is bootstrapped on the client side, it interprets the URI /foo/bar/baz and routes to the appropriate partial.

      We are using rewrite to achieve this in an elegant way and not have to reinvent the wheel. In rewrite the @RewriteConfiguration annotation is what causes the PushStateConfigurationProvider class to be loaded.

      I’m using maven for dependency management, please refer to the maven xml in the article for the rewrite artifact information, and I’ve included the dependencies in this gist for you here: https://gist.github.com/mdanter/a786e447792180958b95

      P.S. I apologize for the late response, your message landed in spam.

  2. john says:

    How is redirecting everything to the index seo friendly?

    1. This uses an internal server forward, not a redirect, so the user’s browser URL never changes.

  3. Eric says:

    Awesome! Works beautifully.

  4. Derk says:

    How can I implement this in dropwizard for example (Jetty)?

  5. rohan says:

    First of all .. thanks for great solution (y)

    I tried this solution but there some issue while i was running tomcat. It is going in infinite loop

    Exceptions:-

    18:00:57.477 [http-nio-8080-exec-8] DEBUG o.s.web.servlet.DispatcherServlet - Successfully completed request
    vletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    	at javax.servlet.ServletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    	at javax.servlet.ServletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    	at javax.servlet.ServletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    	at javax.servlet.ServletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    	at javax.servlet.ServletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    	at javax.servlet.ServletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    	at javax.servlet.ServletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    	at javax.servlet.ServletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    	at javax.servlet.ServletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    	at javax.servlet.ServletRequestWrapper.isAsyncStarted(ServletRequestWrapper.java:409)
    .
    .
    .

    Also,

    at org.apache.catalina.core.ApplicationDispatcher.unwrapRequest(ApplicationDispatcher.java:816)
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:792)
    at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:465)
    at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:390)
    at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:317)
    at org.ocpsoft.rewrite.servlet.impl.HttpRewriteResultHandler.handleResult(HttpRewriteResultHandler.java:41)
    at org.ocpsoft.rewrite.servlet.RewriteFilter.rewrite(RewriteFilter.java:268)
    at org.ocpsoft.rewrite.servlet.RewriteFilter.doFilter(RewriteFilter.java:188)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:719)
    at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:465)
    at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:390)
    at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:317)
    at org.ocpsoft.rewrite.servlet.impl.HttpRewriteResultHandler.handleResult(HttpRewriteResultHandler.java:41)
    at org.ocpsoft.rewrite.servlet.RewriteFilter.rewrite(RewriteFilter.java:268)
    at org.ocpsoft.rewrite.servlet.RewriteFilter.doFilter(RewriteFilter.java:188)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:719)
    at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:465)
    at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:390)
    at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:317)
    at org.ocpsoft.rewrite.servlet.impl.HttpRewriteResultHandler.handleResult(HttpRewriteResultHandler.java:41)
    at org.ocpsoft.rewrite.servlet.RewriteFilter.rewrite(RewriteFilter.java:268)
    at org.ocpsoft.rewrite.servlet.RewriteFilter.doFilter(RewriteFilter.java:188)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:719)
    at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:465)
    at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:390)
    at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:317)
    at org.ocpsoft.rewrite.servlet.impl.HttpRewriteResultHandler.handleResult(HttpRewriteResultHandler.java:41)
    at org.ocpsoft.rewrite.servlet.RewriteFilter.rewrite(RewriteFilter.java:268)
    at org.ocpsoft.rewrite.servlet.RewriteFilter.doFilter(RewriteFilter.java:188)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    .
    .
    .
    .

  6. Rohan says:

    I tried this for my web app. But still my web app is not crawlable.

  7. Mark says:

    Thanks for the article Matyas! Right now our site is hosted on Amazon S3 which doesn’t allow URL rewrites and only redirects so I trust there’s no solution with Amazon S3 + CloudFront to make an angularJS web app SEO friendly? The only confusing part is that if you search site:offtherecord.com in google you’ll notice 2 pages get indexed.

    1. offtherecord.com as expected
    2. offtherecord.com/how-it-works

    so I’m wondering if there’s a way to tell AmazonS3 to not redirect but to somehow do an URL rewrite. I know AmazonS3 is not a webserver and was never intended to be but hosting everything on AmazonS3 is really appealing.

Leave a Comment




Please note: In order to submit code or special characters, wrap it in

[code lang="xml"][/code]
(for your language) - or your tags will be eaten.

Please note: Comment moderation is enabled and may delay your comment from appearing. There is no need to resubmit your comment.