2

I am using Jhipster Spring boot + angular 6. But i'm having trouble because of the hash(#) in URL. It is affecting SEO.

I tried setting useHash: false in app-routing-module.ts. But then the API is not working when I run the project via npm start.

I think somewhere in Java files I have to change a configuration to remove # from the URL.

Here is my WebConfigurer code,

@Configuration
public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer<WebServerFactory> {

    private final Logger log = LoggerFactory.getLogger(WebConfigurer.class);

    private final Environment env;

    private final JHipsterProperties jHipsterProperties;

    private MetricRegistry metricRegistry;

    public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties) {

        this.env = env;
        this.jHipsterProperties = jHipsterProperties;
    }

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        if (env.getActiveProfiles().length != 0) {
            log.info("Web application configuration, using profiles: {}", (Object[]) env.getActiveProfiles());
        }
        EnumSet<DispatcherType> disps = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC);
        initMetrics(servletContext, disps);
        log.info("Web application fully configured");
    }

    /**
     * Customize the Servlet engine: Mime types, the document root, the cache.
     */
    @Override
    public void customize(WebServerFactory server) {
        setMimeMappings(server);

        /*
         * Enable HTTP/2 for Undertow - https://twitter.com/ankinson/status/829256167700492288
         * HTTP/2 requires HTTPS, so HTTP requests will fallback to HTTP/1.1.
         * See the JHipsterProperties class and your application-*.yml configuration files
         * for more information.
         */
        if (jHipsterProperties.getHttp().getVersion().equals(JHipsterProperties.Http.Version.V_2_0) &&
            server instanceof UndertowServletWebServerFactory) {

            ((UndertowServletWebServerFactory) server)
                .addBuilderCustomizers(builder ->
                    builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true));
        }
    }

    private void setMimeMappings(WebServerFactory server) {
        if (server instanceof ConfigurableServletWebServerFactory) {
            MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT);
            // IE issue, see https://github.com/jhipster/generator-jhipster/pull/711
            mappings.add("html", MediaType.TEXT_HTML_VALUE + ";charset=" + StandardCharsets.UTF_8.name().toLowerCase());
            // CloudFoundry issue, see https://github.com/cloudfoundry/gorouter/issues/64
            mappings.add("json", MediaType.TEXT_HTML_VALUE + ";charset=" + StandardCharsets.UTF_8.name().toLowerCase());
            ConfigurableServletWebServerFactory servletWebServer = (ConfigurableServletWebServerFactory) server;
            servletWebServer.setMimeMappings(mappings);
        }
    }

    /**
     * Initializes Metrics.
     */
    private void initMetrics(ServletContext servletContext, EnumSet<DispatcherType> disps) {
        log.debug("Initializing Metrics registries");
        servletContext.setAttribute(InstrumentedFilter.REGISTRY_ATTRIBUTE,
            metricRegistry);
        servletContext.setAttribute(MetricsServlet.METRICS_REGISTRY,
            metricRegistry);

        log.debug("Registering Metrics Filter");
        FilterRegistration.Dynamic metricsFilter = servletContext.addFilter("webappMetricsFilter",
            new InstrumentedFilter());

        metricsFilter.addMappingForUrlPatterns(disps, true, "/*");
        metricsFilter.setAsyncSupported(true);

        log.debug("Registering Metrics Servlet");
        ServletRegistration.Dynamic metricsAdminServlet =
            servletContext.addServlet("metricsServlet", new MetricsServlet());

        metricsAdminServlet.addMapping("/management/metrics/*");
        metricsAdminServlet.setAsyncSupported(true);
        metricsAdminServlet.setLoadOnStartup(2);
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = jHipsterProperties.getCors();
        if (config.getAllowedOrigins() != null && !config.getAllowedOrigins().isEmpty()) {
            log.debug("Registering CORS filter");
            source.registerCorsConfiguration("/api/**", config);
            source.registerCorsConfiguration("/management/**", config);
            source.registerCorsConfiguration("/v2/api-docs", config);
        }
        return new CorsFilter(source);
    }

    @Autowired(required = false)
    public void setMetricRegistry(MetricRegistry metricRegistry) {
        this.metricRegistry = metricRegistry;
    }
}

Here is my AngularRouteFilter servlet code,

public class AngularRouteFilter extends OncePerRequestFilter {

    // add the values you want to redirect for
    private static final Pattern PATTERN = Pattern.compile("^/((api|swagger-ui|management|swagger-resources)/|favicon\\.ico|v2/api-docs).*");

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
        throws ServletException, IOException {
        if (isServerRoute(request)) {
            filterChain.doFilter(request, response);
        } else {
            RequestDispatcher rd = request.getRequestDispatcher("/");
            rd.forward(request, response);
        }
    }

    protected static boolean isServerRoute(HttpServletRequest request) {
        if (request.getMethod().equals("GET")) {
            String uri = request.getRequestURI();
        if (uri.startsWith("/app")){

                return true;
            }
            return PATTERN.matcher(uri).matches();
        }
        return true;
    }
}

here is my Swagger index.html(swagger-ui/index.html)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Swagger UI</title>
    <link rel="icon" type="image/png" href="images/favicon-32x32.png" sizes="32x32" />
    <link rel="icon" type="image/png" href="images/favicon-16x16.png" sizes="16x16" />
    <link href='./dist/css/typography.css' media='screen' rel='stylesheet' type='text/css'/>
    <link href='./dist/css/reset.css' media='screen' rel='stylesheet' type='text/css'/>
    <link href='./dist/css/screen.css' media='screen' rel='stylesheet' type='text/css'/>
    <link href='./dist/css/reset.css' media='print' rel='stylesheet' type='text/css'/>
    <link href='./dist/css/print.css' media='print' rel='stylesheet' type='text/css'/>
    <script src='./dist/lib/object-assign-pollyfill.js' type='text/javascript'></script>
    <script src='./dist/lib/jquery-1.8.0.min.js' type='text/javascript'></script>
    <script src='./dist/lib/jquery.slideto.min.js' type='text/javascript'></script>
    <script src='./dist/lib/jquery.wiggle.min.js' type='text/javascript'></script>
    <script src='./dist/lib/jquery.ba-bbq.min.js' type='text/javascript'></script>
    <script src='./dist/lib/handlebars-4.0.5.js' type='text/javascript'></script>
    <script src='./dist/lib/lodash.min.js' type='text/javascript'></script>
    <script src='./dist/lib/backbone-min.js' type='text/javascript'></script>
    <script src='./dist/swagger-ui.min.js' type='text/javascript'></script>
    <script src='./dist/lib/highlight.9.1.0.pack.js' type='text/javascript'></script>
    <script src='./dist/lib/highlight.9.1.0.pack.js' type='text/javascript'></script>
    <script src='./dist/lib/jsoneditor.min.js' type='text/javascript'></script>
    <script src='./dist/lib/marked.js' type='text/javascript'></script>
    <script src='./dist/lib/swagger-oauth.js' type='text/javascript'></script>

    <!-- Some basic translations -->
    <!-- <script src='lang/translator.js' type='text/javascript'></script> -->
    <!-- <script src='lang/ru.js' type='text/javascript'></script> -->
    <!-- <script src='lang/en.js' type='text/javascript'></script> -->

    <script type="text/javascript">
        $(function() {
            var springfox = {
                "baseUrl": function() {
                    var urlMatches = /(.*)\/swagger-ui\/index.html.*/.exec(window.location.href);
                    return urlMatches[1];
                },
                "securityConfig": function(cb) {
                    $.getJSON(this.baseUrl() + "/swagger-resources/configuration/security", function(data) {
                        cb(data);
                    });
                },
                "uiConfig": function(cb) {
                                    alert(cb);

                    $.getJSON(this.baseUrl() + "/swagger-resources/configuration/ui", function(data) {
                        cb(data);
                    });
                }
            };
            window.springfox = springfox;
            window.oAuthRedirectUrl = springfox.baseUrl() + './dist/o2c.html'

            window.springfox.uiConfig(function(data) {
                window.swaggerUi = new SwaggerUi({
                    dom_id: "swagger-ui-container",
                    validatorUrl: data.validatorUrl,
                    supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
                    onComplete: function(swaggerApi, swaggerUi) {
                        initializeSpringfox();
                        if (window.SwaggerTranslator) {
                            window.SwaggerTranslator.translate();
                        }
                        $('pre code').each(function(i, e) {
                            hljs.highlightBlock(e)
                        });
                    },
                    onFailure: function(data) {
                        log("Unable to Load SwaggerUI");
                    },
                    docExpansion: "none",
                    apisSorter: "alpha",
                    showRequestHeaders: false
                });

                initializeBaseUrl();

                $('#select_baseUrl').change(function() {
                    window.swaggerUi.headerView.trigger('update-swagger-ui', {
                        url: $('#select_baseUrl').val()
                    });
                    addApiKeyAuthorization();
                });

                function addApiKeyAuthorization() {
                    var authToken = JSON.parse(localStorage.getItem("jhi-authenticationtoken") || sessionStorage.getItem("jhi-authenticationtoken"));
                    var apiKeyAuth = new SwaggerClient.ApiKeyAuthorization("Authorization", "Bearer " + authToken, "header");
                    window.swaggerUi.api.clientAuthorizations.add("bearer", apiKeyAuth);
                }

                function getCSRF() {
                    var name = "XSRF-TOKEN=";
                    var ca = document.cookie.split(';');
                    for(var i=0; i<ca.length; i++) {
                        var c = ca[i];
                        while (c.charAt(0)==' ') c = c.substring(1);
                        if (c.indexOf(name) !== -1) return c.substring(name.length,c.length);
                    }
                    return "";
                }

                function log() {
                    if ('console' in window) {
                        console.log.apply(console, arguments);
                    }
                }

                function oAuthIsDefined(security) {
                    return security.clientId
                    && security.clientSecret
                    && security.appName
                    && security.realm;
                }

                function initializeSpringfox() {
                    var security = {};
                    window.springfox.securityConfig(function(data) {
                        security = data;
                        if (typeof initOAuth === "function" && oAuthIsDefined(security)) {
                            initOAuth(security);
                        }
                    });
                }
            });

            function maybePrefix(location, withRelativePath) {
                var pat = /^https?:\/\//i;
                if (pat.test(location)) {
                    return location;
                }
                return withRelativePath + location;
            }

            function initializeBaseUrl() {
                var relativeLocation = springfox.baseUrl();

                $('#input_baseUrl').hide();

                $.getJSON(relativeLocation + "/swagger-resources", function(data) {

                    var $urlDropdown = $('#select_baseUrl');
                    $urlDropdown.empty();
                    $.each(data, function(i, resource) {
                        var option = $('<option></option>')
                        .attr("value", maybePrefix(resource.location, relativeLocation))
                        .text(resource.name + " (" + resource.location + ")");
                        $urlDropdown.append(option);
                    });
                    $urlDropdown.change();
                });

            }

        });
    </script>
</head>


<body class="swagger-section">
<div id='header'>
    <div class="swagger-ui-wrap">
        <a id="logo" href="http://swagger.io">swagger</a>

        <form id='api_selector'>
            <div class='input'>
                <select id="select_baseUrl" name="select_baseUrl"></select>
            </div>
            <div class='input'><input placeholder="http://example.com/api" id="input_baseUrl" name="baseUrl" type="text"/>
            </div>
        </form>
    </div>
</div>

<div id="message-bar" class="swagger-ui-wrap" data-sw-translate>&nbsp;</div>
<div id="swagger-ui-container" class="swagger-ui-wrap"></div>
</body>
</html>

here is docs.component.html

<iframe src="swagger-ui/index.html" width="100%" height="900" seamless
    target="_top" title="Swagger UI" class="border-0"></iframe>

here my server code is running perfectly @ localhost:6060. Butm localhost:6060/api/docs opening a blank page.

here is the screen shot,

enter image description here

Please suggest me where i am doing wrong.

SFDC
  • 76
  • 2
  • 14
  • Yeah, i saw that post already. But, unable to understand how and where should i modify – SFDC Jan 10 '19 at 15:19
  • @GaëlMarziou i have added the code as per the suggestion in that link. But, stuck where should i call that class ? here is the code screen shot https://www.dropbox.com/s/plqfkde7cvn9bw6/Screen%20Shot%202019-01-14%20at%208.03.07%20PM.png?dl=0 – SFDC Jan 14 '19 at 14:35
  • Please suggest me where to call this class to activate without hash @GaëlMarziou – SFDC Jan 14 '19 at 14:36
  • @GaëlMarziou Using this link https://github.com/jhipster/generator-jhipster/issues/4794#issuecomment-304097246, i added the code but confused where to call this class – SFDC Jan 14 '19 at 14:41
  • @GaëlMarziou, i have edited my question with code. Please suggest me on which line should i call ? i am stuck in this from past 2 weeks. – SFDC Jan 14 '19 at 16:32
  • @GaëlMarziou, i did as you suggested, but it's now working :( https://www.dropbox.com/s/uity4aqyqg37y4c/Screen%20Shot%202019-01-16%20at%206.01.20%20PM.png?dl=0 – SFDC Jan 16 '19 at 12:32
  • Anything am missing @GaëlMarziou – SFDC Jan 16 '19 at 12:33
  • @GaëlMarziou, I can see the logs in console DEBUG 2115 --- [ restartedMain] c.c.library.config.AngularRouteFilter : Initializing filter 'angularRouteFilter' and then DEBUG 2115 --- [ restartedMain] c.c.library.config.AngularRouteFilter : Filter 'angularRouteFilter' configured successfully .. but in the webpage, nothing is opening. Check my screen shot here https://www.dropbox.com/s/q55wdfxnrjt5kay/Screen%20Shot%202019-01-16%20at%207.08.26%20PM.png?dl=0 – SFDC Jan 16 '19 at 13:39
  • Please suggest me where am doing wrong to achieve this @GaëlMarziou.. – SFDC Jan 16 '19 at 14:28
  • @GaëlMarziou, please help me where am doing wrong – SFDC Jan 16 '19 at 17:30
  • I had put the breakpoint as suggested in doFilterInternal() . Please check the screen shot https://www.dropbox.com/s/1lqkojeufrfirqq/Screen%20Shot%202019-01-16%20at%2011.19.20%20PM.png?dl=0 – SFDC Jan 16 '19 at 17:50
  • Using java server port it's working fine. But, http://localhost:6060/admin/docs where we can check the API's not opening the HTML in it. please suggest me anything else i should configure @GaëlMarziou https://www.dropbox.com/s/wlh3rf3bz4ztuez/Screen%20Shot%202019-01-17%20at%203.13.19%20PM.png?dl=0 – SFDC Jan 17 '19 at 09:40
  • @GaëlMarziou, here is the screen shot of UI when i run localhost:6060/admin/docs in the browswer https://www.dropbox.com/s/oala292few5i3l9/Screen%20Shot%202019-01-17%20at%203.24.51%20PM.png?dl=0 – SFDC Jan 17 '19 at 09:56
  • Please suggest me @GaëlMarziou – SFDC Jan 17 '19 at 15:51
  • API docs page is special because it runs in an iframe that needs to download anything from /swagger-ui, i.e it's not a part of your angular app. So make sure you see the broswer console of the iframe to debug what is missing. Update your code in the question. – Gaël Marziou Jan 17 '19 at 17:57
  • Updated code in the question. please check and suggest me @GaëlMarziou – SFDC Jan 17 '19 at 20:49
  • What should i change to get admin/docs @GaëlMarziou .. please suggest me – SFDC Jan 19 '19 at 02:05
  • I posted an answer that I tested. You should delete all the comments above as they will just confuse future readers. – Gaël Marziou Jan 19 '19 at 10:55

3 Answers3

4

Solution using a servlet filter

First step is to configure client, set useHash: false in app-routing-module.ts

In index.html, change <base href="./" /> to <base href="/" />

But it's not enough because it does not support deep linking which means linking to a client route from an external link like from a mail message or from another web site, or veen when refreshing page in browser.

When deep linking, the server receives the request first and if the URL has to be handled by client side, it will not be found so our server app must detect whether the URL has to be served as-is by server (e.g. all API calls) or forwarded to index.html so that the javascript app can interpret it.

One solution is to use pattern matching in a servlet filter (Html5RouteFilter) that we register in WebConfigurer.java

WebConfigurer.java

    @Bean
    public Html5RouteFilter html5RouteFilter() {
        return new Html5RouteFilter();
    }

Html5RouteFilter.java

package com.mycompany.myapp.web;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.regex.Pattern;

/**
 * Filter that distinguishes between client routes and server routes  when you don't use '#' in client routes.
 */
public class Html5RouteFilter extends OncePerRequestFilter {

    private Logger log = LoggerFactory.getLogger(getClass());


    // These are the URIs that should be processed server-side
    private static final Pattern PATTERN = Pattern.compile("^/((api|content|i18n|management|swagger-ui|swagger-resources)/|error|h2-console|swagger-resources|favicon\\.ico|v2/api-docs).*");

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
        throws ServletException, IOException {
        if (isServerRoute(request)) {
            filterChain.doFilter(request, response);
        } else {
            RequestDispatcher rd = request.getRequestDispatcher("/");
            rd.forward(request, response);
        }
    }

    protected static boolean isServerRoute(HttpServletRequest request) {
        if (request.getMethod().equals("GET")) {
            String uri = request.getRequestURI();
            if (uri.startsWith("/app")) {
                return true;
            }
            return PATTERN.matcher(uri).matches();
        }
        return true;
    }
}
Gaël Marziou
  • 16,028
  • 4
  • 38
  • 49
  • I've tried this and the REST controller approach and I'm still getting 404s when navigating to routes manually. Is there any additional trick to getting this to work on Angular 6 and Spring Boot 2.0? I see the issue when running locally and when deploying to Heroku. – Mike Feb 21 '19 at 13:47
  • Have a look at this PR https://github.com/jhipster/generator-jhipster/pull/9098 until it gets released – Gaël Marziou Feb 21 '19 at 14:41
  • You are a lifesaver, that PR had all the changes I needed. Finally, I have shareable URLs without requiring hashtags :) – Mike Feb 21 '19 at 18:21
  • One update for anyone else who finds this. The filter approach fixed my routing issues once I applied the other changes from the above PR. However, it created a new issue when I deployed to Prod: some of my CSS assets weren't loading due to the following error: `Refused to apply style from because its MIME type is ('text/html') ... and strict checking enabled`. Not sure it's the right long term fix, but I ended up modifying the filter code above to check for css files and make sure they get handled on the server. – Mike Feb 22 '19 at 03:57
  • I used this approach but the return page is blank, perhaps js scripts aren't loading. at the bottom of the page, there is no reference to angular like it used to be with #. Do you know maybe how to fix this? – Kyrylo Lukeniuk Nov 13 '19 at 12:10
  • Have you looked at the browser's console and/or at server logs? Which version of JHipster? Which code did you copy : from this answer of from the PR in comments above? – Gaël Marziou Nov 13 '19 at 12:43
3

Solution using a REST controller

First step is to configure client, set useHash: false in app-routing-module.ts

In index.html, change <base href="./" /> to <base href="/" />

But it's not enough because it does not support deep linking which means linking to a client route from an external link like from a mail message or from another web site, or veen when refreshing page in browser.

When deep linking, the server receives the request first and if the URL has to be handled by client side, it will not be found so our server app must detect whether the URL has to be served as-is by server (e.g. all API calls) or forwarded to index.html so that the javascript app can interpret it.

ClientRouteForwarder .java

package com.mycompany.myapp.web.rest;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * A REST controller that forwards all GET requests that did not match a RequestMapping to /index.html so that
 * client routes can be handled by client code in browser.
 *
 * This works because Spring resolves exact matches first.
 */
@Controller
public class ClientRouteForwarder {

    @GetMapping(value = "/**/{[path:[^\\.]*}")
    public String forward() {
        return "forward:/";
    }
}
Gaël Marziou
  • 16,028
  • 4
  • 38
  • 49
  • I used this approach but the return page is blank, perhaps js scripts aren't loading. at the bottom of the page, there is no reference to angular like it used to be with #. Do you know maybe how to fix this? – Kyrylo Lukeniuk Nov 13 '19 at 12:05
0

As per their official documentation Configuring html5, AngularJS uses a “#” in it’s urls. HTML5Mode of AngularJS removes these “#” from URL.

Activate HTML 5 Mode

Create html5.mode.config.js file in webapp/app/blocks/config/ directory:

(function() {
  'use strict';

  angular
    .module('<YourAppName>')
    .config(html5ModeConfig);

  html5ModeConfig.$inject = ['$locationProvider'];

  function html5ModeConfig($locationProvider) {
    $locationProvider.html5Mode({ enabled: true, requireBase: true });
  }
})();

Then open index.html and add this line in head tag:

<base href="/">
kj007
  • 6,073
  • 4
  • 29
  • 47
  • when i am changing hash = false in app-routing-module.ts file, UI is working fine. But API's are not fetching – SFDC Jan 10 '19 at 14:30
  • is there any error at spring side?? or could you check what api url is being passed?? you may check by developer console tool by inspecting or logging requests at spring side. – kj007 Jan 10 '19 at 14:33
  • if you don't mind, can u come over teamviewer ? so that i can show my screen and explain – SFDC Jan 10 '19 at 14:44
  • if i replace usehash= false , i am getting pages and everything, onclick it's navigating well and good. But, if i refresh the page Your request cannot be processed Sorry, an error has occurred. Status: Not Found (Not Found) Message: Not Found – SFDC Jan 10 '19 at 15:03
  • It's not as easy you need to add a servlet filter java side, see https://stackoverflow.com/questions/48567071/jhipster-refresh-url-cause-cannot-get-user-management – Gaël Marziou Jan 10 '19 at 15:14