- ui-router 2017-03-19T16:58:15+01:00 https://fadeit.dk/blog/tag/ui-router.html AngularJS SEO friendly translation URLs with ui-router 2015-03-30T18:10:55+02:00 https://fadeit.dk/blog /2015/03/30/angular-translate-ui-router-seo <h2 id="introduction">Introduction</h2> <p>In a <a href="https://fadeit.dk/blog/post/angularjs-seo-for-angular-translate">previous</a> post we explored how to introduce unique URLs per language using primarily NGINX and Grunt configuration. To recap, that approach is reasonable if you already have a working application and prefer not to re-factor every state and view that links to it. While it may be easier to implement in an existing application, it forces users to perform a refresh when switching between languages - though not a big issue (how often do users switch back-and-forth anyway), having a Single Page Application (SPA) that forces users to reload pages is sub-optimal. In this article we will explore possibilities of implementing SEO friendly URLs using ui-router.</p> <p>Nested states is the key to introducing language as a URL variable to all states. We start off by creating the ‘app’ state that every other state will be child of. The app state is abstract, so that the state by itself can not be activated. Next we want the route to only match the languages we intend to support (in this example ‘da’ and ‘en’) to reduce possible false-positive matches e.g example.com/<b>da</b>ta. As ui-router injects the content of the child states into ui-view, we define it as an inline template for the parent route. We also define ‘app.home’ state that will handle example.com/en and example.com/da pages:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app'</span><span class="p">,</span> <span class="p">{</span> <span class="na">abstract</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">url</span><span class="p">:</span> <span class="s1">'/{lang:(?:da|en)}'</span><span class="p">,</span> <span class="na">template</span><span class="p">:</span> <span class="s1">'&lt;ui-view/&gt;'</span> <span class="p">});</span> <span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app.home'</span><span class="p">,</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'views/home-page.html'</span><span class="p">,</span> <span class="p">});</span> </code></pre> </div> <p>Now every state that we want to translate should be a child of app state:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app.about'</span><span class="p">,</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="s1">'/about'</span><span class="p">,</span> <span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'views/about-page.html'</span><span class="p">,</span> <span class="p">});</span> </code></pre> </div> <p>Since the child states contain a language path variable, we need to alter the links pointing to the child states to pass in <code class="highlighter-rouge">lang</code> parameter. But before we do so, we need to set <code class="highlighter-rouge">activeLang</code> on <code class="highlighter-rouge">$rootScope</code> so we can read it from there when generating links with <code class="highlighter-rouge">ui-sref</code>. Due to the fact that <code class="highlighter-rouge">activeLang</code> can change at state change, we need to subscribe to <code class="highlighter-rouge">stateChangeSuccess</code> event. While at it, we can also generate links for the same page in other languages - ‘In English’ link at example.com/da/about should point to example.com/en/about. In this simple example we use only two languages, so we’ll be using only one variable <code class="highlighter-rouge">otherLangURL</code>:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">navigationController</span><span class="p">(</span><span class="nx">$scope</span><span class="p">,</span> <span class="nx">$rootScope</span><span class="p">,</span> <span class="nx">$stateParams</span><span class="p">,</span> <span class="nx">$translate</span><span class="p">){</span> <span class="nx">$scope</span><span class="p">.</span><span class="nx">$on</span><span class="p">(</span><span class="s1">'$stateChangeSuccess'</span><span class="p">,</span> <span class="kd">function</span> <span class="nx">rootStateChangeSuccess</span><span class="p">(</span><span class="nx">event</span><span class="p">,</span> <span class="nx">toState</span><span class="p">){</span> <span class="k">if</span><span class="p">(</span><span class="nx">$stateParams</span><span class="p">.</span><span class="nx">lang</span> <span class="o">!==</span> <span class="kc">undefined</span><span class="p">){</span> <span class="kd">var</span> <span class="nx">otherLang</span> <span class="o">=</span> <span class="nx">$stateParams</span><span class="p">.</span><span class="nx">lang</span> <span class="o">===</span> <span class="s1">'da'</span> <span class="p">?</span> <span class="s1">'en'</span> <span class="p">:</span> <span class="s1">'da'</span><span class="p">;</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="nx">activeLang</span> <span class="o">=</span> <span class="nx">$stateParams</span><span class="p">.</span><span class="nx">lang</span><span class="p">;</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="nx">otherLangURL</span> <span class="o">=</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">absUrl</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'/'</span> <span class="o">+</span> <span class="nx">$stateParams</span><span class="p">.</span><span class="nx">lang</span><span class="p">,</span> <span class="s1">'/'</span> <span class="o">+</span><span class="nx">otherLang</span><span class="p">);</span> <span class="nx">$translate</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">$stateParams</span><span class="p">.</span><span class="nx">lang</span><span class="p">);</span> <span class="p">}</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> <p>It would be nice to use <code class="highlighter-rouge">ui-sref</code> to dynamically generate the links to same page in other languages, but unfortunately <code class="highlighter-rouge">ui-sref</code> does not support variables in state names, therefore we have to fall back to <code class="highlighter-rouge">ng-href</code> for generating the links. Fortunately path variable substitution is supported, therefore we can generate menu links using <code class="highlighter-rouge">activeLang</code> variable we stored on <code class="highlighter-rouge">$rootScope</code>:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"menu-navigation"</span><span class="nt">&gt;</span> ... <span class="nt">&lt;li</span> <span class="na">ui-sref-active=</span><span class="s">"activeMenu"</span><span class="nt">&gt;</span> <span class="nt">&lt;a</span> <span class="na">href</span> <span class="na">ui-sref=</span><span class="s">"app.about({lang: activeLang})"</span> <span class="na">translate</span><span class="nt">&gt;</span> NAV_ABOUT <span class="nt">&lt;/a&gt;</span> <span class="nt">&lt;/li&gt;</span> <span class="nt">&lt;li&gt;</span> <span class="nt">&lt;a</span> <span class="na">ng-href=</span><span class="s">""</span> <span class="na">translate</span><span class="nt">&gt;</span> IN_LANGUAGE <span class="nt">&lt;/a&gt;</span> <span class="nt">&lt;/li&gt;</span> <span class="nt">&lt;/ul&gt;</span> </code></pre> </div> <h2 id="conclusion">Conclusion</h2> <p>Incorporating language in the application routes from very start will save time in the long run as re-factoring hundreds of <code class="highlighter-rouge">ui-sref</code> links will take it’s time. Ui-router fully supports nested sub-states, so there will not be any conflict if your application is already using nested states, just make sure that there is <code class="highlighter-rouge">ui-view</code> for each parent state to inject the content into. I was unable to make the lang parameter in the parent state optional, thus language will be enforced in every path (as opposed to <a href="https://fadeit.dk/blog/post/angularjs-seo-for-angular-translate">previous post</a> where we were able to use paths without language defined in them). Have a better approach? leave a comment!</p> AngularJS SEO friendly translations 2015-03-12T16:30:16+01:00 https://fadeit.dk/blog /2015/03/12/angularjs-seo-for-angular-translate <p><strong>Update</strong>: I have written a follow-up post solving the same problem using ui-router <a href="https://fadeit.dk/blog/post/angular-translate-ui-router-seo">here</a></p> <h2 id="introduction">Introduction</h2> <p>Instant translations, this is what <a href="http://angular-translate.github.io/">angular-translate</a> brings to the table. While it is significantly more convenient than waiting one more site load cycle for a different language to load (especially on sites that are slow to begin with), it doesn’t come without complications. If each translation language does not have a unique URL, search engines can not link search results to it, let alone crawl the site properly.</p> <p>Here’s what Google has to say on the matter:</p> <blockquote> <p><strong>Make sure each language version is easily discoverable</strong> <em>Keep the content for each language on separate URL’s. Don’t use cookies to show translated versions of the page. Consider cross-linking each language version of a page.</em> <a href="https://support.google.com/webmasters/answer/182192">https://support.google.com/webmasters/answer/182192</a></p> </blockquote> <h2 id="options">Options</h2> <p>Okay, perhaps it’s a nice practice to have language in the URL. Here are the options:</p> <ol> <li>Store the language in a GET parameter e.g: <strong>example.com/about?lang=en</strong></li> <li>Store the language in a path e.g: <strong>example.com/en/about</strong></li> <li>Store the language in a sub-domain e.g: <strong>en.example.com/about</strong></li> </ol> <p>While the first option would be the easiest to implement, it is also the ugliest. The other two are relatively similar in terms of appearance and it boils down to your personal preference. An important distincion to make is that if language is stored in a sub-domain, it excludes the possibility of handling the routing with angular as subdomain is out of application router scope. We decided to explore the second option - having the language in a URL path.</p> <p>The preferred approach would be to introduce a path variable on a router base path and have all children be relative to it or escape the language before routing is performed. While it is possible, it seems that the only way to achieve it is to nest all states in the following manner:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">angular</span><span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'app'</span><span class="p">).</span><span class="nx">config</span><span class="p">([</span> <span class="s1">'$stateProvider'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">$stateProvider</span><span class="p">)</span> <span class="p">{</span> <span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app'</span><span class="p">,</span> <span class="p">{</span> <span class="na">abstract</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">url</span><span class="p">:</span> <span class="s1">'/{lang:(?:da|en)}'</span><span class="p">,</span> <span class="na">template</span><span class="p">:</span> <span class="s1">'&lt;ui-view/&gt;'</span><span class="p">,</span> <span class="na">controller</span><span class="p">:</span> <span class="s1">'RootController'</span> <span class="p">});</span> <span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app.root'</span><span class="p">,</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'views/home.html'</span><span class="p">,</span> <span class="p">});</span> <span class="nx">$stateProvider</span><span class="p">.</span><span class="nx">state</span><span class="p">(</span><span class="s1">'app.about'</span><span class="p">,</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'views/about.html'</span><span class="p">,</span> <span class="p">});</span> <span class="p">}</span> <span class="p">]);</span> </code></pre> </div> <p>It is likely that by the time SEO becomes a priority, the application is already built and refactoring code does not sound appealing. To name a few, using ui-router has following downsides:</p> <ul> <li>Language slug becomes mandatory for each state, so example.com/about would become example.com/da/about</li> <li>All child state names need to be changed</li> <li>All links have to be refactored</li> </ul> <p>We’ll be taking a closer look at ui-router in a separate post soon, while this article focuses on finding an alternative configuration-based approach.</p> <h2 id="automation-to-the-rescue">Automation to the rescue!</h2> <p>Hosting Angular application is just serving static files, where each URL maps to a directory/file by default. Therefore, if we are to host the same application in a different directory for each language, we get SEO friendly URLs without a major overhaul of the code. If you are working on a big Angular project, the chances are that either Grunt or Gulp is in the mix. Grunt is our weapon of choice, so we need to add an extra copy task at the end of the build process that would copy all files into a subdirectory:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">copy</span><span class="err">:</span> <span class="p">{</span> <span class="nl">build_en_lang</span><span class="p">:</span> <span class="p">{</span> <span class="nl">files</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">src</span><span class="p">:</span> <span class="p">[</span> <span class="s1">'**'</span><span class="p">,</span> <span class="s1">'!en/**'</span><span class="p">],</span> <span class="na">dest</span><span class="p">:</span> <span class="s1">'&lt;%= build_dir %&gt;/en'</span><span class="p">,</span> <span class="na">cwd</span><span class="p">:</span> <span class="s1">'&lt;%= build_dir %&gt;'</span><span class="p">,</span> <span class="na">expand</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">]</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p><code class="highlighter-rouge">!en/**</code> tells Grunt not to copy itself every time that task is called.</p> <p>The directory structure should be following:</p> <ul> <li>/path/to/app/index.html (will default to Danish)</li> <li>/path/to/app/other/files</li> <li>/path/to/app/en/index.html</li> <li>/path/to/app/en/other/files</li> </ul> <p>Now we need to tell Angular to check for URL when determining language:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">absUrl</span> <span class="o">=</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">absUrl</span><span class="p">();</span> <span class="k">if</span><span class="p">(</span><span class="nx">absUrl</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="s1">'/en/'</span><span class="p">)</span> <span class="o">!==</span> <span class="o">-</span><span class="mi">1</span><span class="p">){</span> <span class="nx">$scope</span><span class="p">.</span><span class="nx">activeLang</span> <span class="o">=</span> <span class="s1">'en-us'</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span><span class="p">{</span> <span class="nx">$scope</span><span class="p">.</span><span class="nx">activeLang</span> <span class="o">=</span> <span class="s1">'da-dk'</span><span class="p">;</span> <span class="p">}</span> <span class="nx">$translate</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">$scope</span><span class="p">.</span><span class="nx">activeLang</span><span class="p">);</span> </code></pre> </div> <p><code class="highlighter-rouge">activeLang</code> is stored on scope because it will be later used to determine the class of language switching button</p> <p>Google also suggests cross-linking each language version of the page. If user navigates to a different page, we should also update the link that points to the same page in other languages. For that we use $stateChangeSuccess event:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="c1">//Construct url base</span> <span class="kd">var</span> <span class="nx">port</span> <span class="o">=</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">port</span><span class="p">();</span> <span class="c1">//Port can be omitted on 80 or 443</span> <span class="nx">port</span> <span class="o">=</span> <span class="p">(</span><span class="nx">port</span> <span class="o">===</span> <span class="mi">80</span> <span class="o">||</span> <span class="nx">port</span> <span class="o">===</span> <span class="mi">443</span><span class="p">)</span> <span class="p">?</span> <span class="s1">''</span> <span class="p">:</span> <span class="s1">':'</span> <span class="o">+</span> <span class="nx">port</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">urlBase</span> <span class="o">=</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">protocol</span><span class="p">()</span> <span class="o">+</span> <span class="s1">'://'</span> <span class="o">+</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">host</span><span class="p">()</span> <span class="o">+</span> <span class="nx">port</span><span class="p">;</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="nx">$on</span><span class="p">(</span><span class="s1">'$stateChangeSuccess'</span><span class="p">,</span><span class="kd">function</span><span class="p">(){</span> <span class="nx">$scope</span><span class="p">.</span><span class="nx">enUrl</span> <span class="o">=</span> <span class="nx">urlBase</span> <span class="o">+</span> <span class="s1">'/en'</span> <span class="o">+</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">url</span><span class="p">();</span> <span class="nx">$scope</span><span class="p">.</span><span class="nx">daUrl</span> <span class="o">=</span> <span class="nx">urlBase</span> <span class="o">+</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">url</span><span class="p">();</span> <span class="p">});</span> </code></pre> </div> <p>The URLs need to be absolute, otherwise if this code was executed at example.com/en, it would calculate the ‘en’ URL to be at example.com/en/en and so on.</p> <p>Next we need to display our cross-links in the template using <code class="highlighter-rouge">ng-href</code> directive:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;a</span> <span class="na">ng-href=</span><span class="s">""</span> <span class="na">target=</span><span class="s">"_self"</span> <span class="na">ng-class=</span><span class="s">"{'active': activeLang === 'en-us'}"</span><span class="nt">&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"assets/images/gb.svg"</span> <span class="na">alt=</span><span class="s">"Change language to English"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/a&gt;</span> <span class="nt">&lt;a</span> <span class="na">ng-href=</span><span class="s">""</span> <span class="na">target=</span><span class="s">"_self"</span> <span class="na">ng-class=</span><span class="s">"{'active': activeLang === 'da-dk'}"</span><span class="nt">&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"assets/images/dk.svg"</span> <span class="na">alt=</span><span class="s">"Change language to Danish"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/a&gt;</span> </code></pre> </div> <p><code class="highlighter-rouge">target='_self'</code> is used to tell Angular not to load this link in ajax but to navigate to it the Angular way. We need the browser to fetch appropriate index.html page and bootstrap the application (since we have one application per language).</p> <p>The application is now SEO friendly, but we are not quite done yet. We are also using HTML5 mode to have pretty URLs. When using HTML5 mode, we must define base tag in the index file so Angular knows where the application path starts from:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span> <span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"da"</span> <span class="na">ng-app=</span><span class="s">"example"</span> <span class="na">ng-controller=</span><span class="s">"AppCtrl"</span><span class="nt">&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;base</span> <span class="na">href=</span><span class="s">"/"</span><span class="nt">&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"alternate"</span> <span class="na">hreflang=</span><span class="s">"en"</span> <span class="na">href=</span><span class="s">"http://example.com/en"</span><span class="nt">/&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"alternate"</span> <span class="na">hreflang=</span><span class="s">"da"</span> <span class="na">href=</span><span class="s">"http://example.com"</span><span class="nt">/&gt;</span> ... </code></pre> </div> <p>In case <code class="highlighter-rouge">hreflang</code> caught your eye, you can read more about it <a href="https://support.google.com/webmasters/answer/182192">here</a>.</p> <p>However, now we also have a copy of the site running at /en, so the base tag needs to be appropriate.</p> <h2 id="grunt-to-the-rescue-again">Grunt to the rescue, again!</h2> <p>We need to update the base tag when making a copy of the site to match the path. Grunt’s copy task features a process option that is called for each file copied. The process option enables us to transform the contents of the file:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">copy</span><span class="err">:</span> <span class="p">{</span> <span class="nl">options</span><span class="p">:</span> <span class="p">{</span> <span class="nl">processContent</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">contents</span><span class="p">,</span> <span class="nx">srcpath</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span><span class="p">(</span><span class="nx">srcpath</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="s1">'index.html'</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">){</span> <span class="c1">//note spaces around in order not to match 'hreflang' tag</span> <span class="nx">contents</span> <span class="o">=</span> <span class="nx">contents</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">' lang="da" '</span><span class="p">,</span> <span class="s1">' lang="en" '</span><span class="p">);</span> <span class="nx">contents</span> <span class="o">=</span> <span class="nx">contents</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'&lt;base href="/"&gt;'</span><span class="p">,</span> <span class="s1">'&lt;base href="/en/"&gt;'</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">contents</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">//build_en_lang omitted</span> <span class="p">}</span> </code></pre> </div> <p>We are using older version of Grunt, so the option is called <code class="highlighter-rouge">processContent</code> instead of <code class="highlighter-rouge">process</code>.</p> <p>HTML5 mode also requires URL rewriting on the server side. If a request is made at example.com/about, the server should still return the index.html page and it is up to Angular router to figure out what to do with the ‘about’ path variable. In Nginx, this is typically achieved with try_files directive:</p> <div class="language-bash highlighter-rouge"><pre class="highlight"><code>location / <span class="o">{</span> try_files <span class="nv">$uri</span> <span class="nv">$uri</span>/ <span class="nv">$uri</span>/index.html /index.html; <span class="o">}</span> </code></pre> </div> <p>If a request is made at example.com/en/about, the configuration above will result in Nginx trying following files:</p> <ul> <li>en/about –&gt; not found</li> <li>en/about/ –&gt; not found</li> <li>en/about/index.html –&gt; not found</li> <li>index.html –&gt; found</li> </ul> <p>This results in the server returning the index.html file of the root rather than of ‘en’ directory. Now the router is trying to figure out which state to match the <code class="highlighter-rouge">en/about</code> path to, which it will of course fail to do accomplish. We need to define another location to load proper index.html, if $uri is not a match:</p> <div class="language-bash highlighter-rouge"><pre class="highlight"><code>location /en/ <span class="o">{</span> try_files <span class="nv">$uri</span> <span class="nv">$uri</span>/ <span class="nv">$uri</span>/index.html /en/index.html; <span class="o">}</span> </code></pre> </div> <p>Finally, SEO friendly multilingual application!<br />Now that we’ve established that there is no way around configuring web server, let’s make most of it! Since the only file that actually changed is index.html, there is no real need to copy other resources. We start out by changing the grunt copy task to only copy index file to same directory and rename it to index.en.html:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">copy</span><span class="p">{</span> <span class="nl">build_en_lang</span><span class="p">:</span> <span class="p">{</span> <span class="nl">options</span><span class="p">:</span> <span class="p">{</span> <span class="nl">processContent</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">contents</span><span class="p">)</span> <span class="p">{</span> <span class="c1">//note spaces around in order not to match 'hreflang' tag</span> <span class="nx">contents</span> <span class="o">=</span> <span class="nx">contents</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">' lang="da" '</span><span class="p">,</span> <span class="s1">' lang="en" '</span><span class="p">);</span> <span class="nx">contents</span> <span class="o">=</span> <span class="nx">contents</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'&lt;base href="/"&gt;'</span><span class="p">,</span> <span class="s1">'&lt;base href="/en/"&gt;'</span><span class="p">);</span> <span class="k">return</span> <span class="nx">contents</span><span class="p">;</span> <span class="p">},</span> <span class="p">},</span> <span class="nx">files</span><span class="err">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">src</span><span class="p">:</span> <span class="p">[</span> <span class="s1">'index.html'</span><span class="p">],</span> <span class="na">dest</span><span class="p">:</span> <span class="s1">'&lt;%= build_dir %&gt;/'</span><span class="p">,</span> <span class="na">cwd</span><span class="p">:</span> <span class="s1">'&lt;%= build_dir %&gt;'</span><span class="p">,</span> <span class="na">expand</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">rename</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">dest</span><span class="p">,</span> <span class="nx">src</span><span class="p">){</span> <span class="k">return</span> <span class="nx">dest</span> <span class="o">+</span> <span class="nx">src</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'.html'</span><span class="p">,</span> <span class="s1">'.en.html'</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="p">]</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Now Nginx ‘en’ location needs to serve index.en.html instead of index.html, otherwise it should ignore /en/ part of the URL when serving static files. We will use regular expression to match /en and capture everything that comes after into $1. Then we use captured group in try_files directive:</p> <div class="language-bash highlighter-rouge"><pre class="highlight"><code>location ~ ^/en/?<span class="o">(</span>.<span class="k">*</span><span class="o">)</span><span class="nv">$ </span><span class="o">{</span> index index.en.html; try_files /<span class="nv">$1</span> /<span class="nv">$1</span>/ /index.en.html; <span class="o">}</span> </code></pre> </div> <p>We have jumped through several hoops in order to get rid of the comfort angular-translate provides. Fortunately we can bring some of it back - authenticated users are not crawlers, so we can display ‘quick buttons’ for them while displaying SEO friendly cross-links to unauthenticated users:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">ng-if=</span><span class="s">"!auth.isAuthenticated"</span><span class="nt">&gt;</span> <span class="nt">&lt;a</span> <span class="na">ng-href=</span><span class="s">""</span> <span class="na">target=</span><span class="s">"_self"</span> <span class="na">ng-class=</span><span class="s">"{'active': activeLang === 'en-us'}"</span><span class="nt">&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"assets/images/gb.svg"</span> <span class="na">alt=</span><span class="s">"Change language to English"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/a&gt;</span> ... <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;div</span> <span class="na">ng-if=</span><span class="s">"auth.isAuthenticated"</span><span class="nt">&gt;</span> <span class="nt">&lt;a</span> <span class="na">ng-href=</span><span class="s">"#"</span> <span class="na">ng-click=</span><span class="s">"changeLang('en-us')"</span> <span class="na">ng-class=</span><span class="s">"{'active': activeLang === 'en-us'}"</span><span class="nt">&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"assets/images/gb.svg"</span> <span class="na">alt=</span><span class="s">"Change language to English"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/a&gt;</span> ... <span class="nt">&lt;/div&gt;</span> </code></pre> </div> <h2 id="conclusion">Conclusion</h2> <p>With this approach we have tried to solve as much as possible with configuration, so we’ve managed not to entangle language awareness with every state in the application. While ui-router configuration would be preferred, it does not come without limitations: ui-router can not be used for sub-domains and ui-router enforces using language in every url rather than having it as optional parameter.</p> <p>At the end of day there is no good way to to play by SEO rules, so hopefully AngularJS 2.0 will change this.</p>