In the previous post Use Prerender to improve AngularJS SEO, I have explained different solutions at 3 different levels to implement Prerender.javascript
In this post, I will explain how to implement a ASP.NET Core Middleware as a application level middleware to implement prerender. css
At first, let's review what's the appliaction level middleware solution architecture.html
In ASP.NET Core, we can create a Middleware, which has the similar functionality as HttpModule in ASP.NET, but in ASP.NET Core, there is no interface or base class we can use to declare a Middleware.java
The default convention is that, we need to:git
So, the class is as below. I have added PrerenderConfiguration for getting configuration.angularjs
#region Ctor public PrerenderMiddleware(RequestDelegate next, PrerenderConfiguration configuration) { _next = next; Configuration = configuration; } #endregion #region Properties public PrerenderConfiguration Configuration { get; private set; } #endregion #region Invoke public async Task Invoke(HttpContext httpContext) { await Prerender(httpContext); } #endregion
Then, we need to implement Prerender(httpContext) logicgithub
If you know my implementation for PrerenderHttpModule in ASP.NET, I used HttpWebRequest & HttpWebResponse.web
But for PrerenderMiddleware here, I use HttpClient, as with the HttpWebRequest in ASP.NET Core (at 2/11/2017), there is no way to setup AllowAutoRedirect and other http headers.json
private async Task Prerender(HttpContext httpContext) { var request = httpContext.Request; var response = httpContext.Response; var requestFeature = httpContext.Features.Get<IHttpRequestFeature>(); if (IsValidForPrerenderPage(request, requestFeature)) { // generate URL var requestUrl = request.GetDisplayUrl(); // if traffic is forwarded from https://, we convert http:// to https://. if (string.Equals(request.Headers[Constants.HttpHeader_XForwardedProto], Constants.HttpsProtocol, StringComparison.OrdinalIgnoreCase) && requestUrl.StartsWith(Constants.HttpProtocol, StringComparison.OrdinalIgnoreCase)) { requestUrl = Constants.HttpsProtocol + requestUrl.Substring(Constants.HttpProtocol.Length); } var prerenderUrl = $"{Configuration.ServiceUrl.Trim('/')}/{requestUrl}"; // use HttpClient instead of HttpWebRequest, as HttpClient has AllowAutoRedirect option. var httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = true }; // Proxy Information if (!string.IsNullOrEmpty(Configuration.ProxyUrl) && Configuration.ProxyPort > 0) httpClientHandler.Proxy = new WebProxy(Configuration.ProxyUrl, Configuration.ProxyPort); using (var httpClient = new HttpClient(httpClientHandler)) { httpClient.Timeout = TimeSpan.FromSeconds(60); httpClient.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() { NoCache = true }; httpClient.DefaultRequestHeaders.TryAddWithoutValidation(Constants.HttpHeader_ContentType, "text/html"); httpClient.DefaultRequestHeaders.TryAddWithoutValidation(Constants.HttpHeader_UserAgent, request.Headers[Constants.HttpHeader_UserAgent].ToString()); if (!string.IsNullOrEmpty(Configuration.Token)) httpClient.DefaultRequestHeaders.TryAddWithoutValidation(Constants.HttpHeader_XPrerenderToken, Configuration.Token); using (var webMessage = await httpClient.GetAsync(prerenderUrl)) { var text = default(string); try { response.StatusCode = (int)webMessage.StatusCode; foreach (var keyValue in webMessage.Headers) { response.Headers[keyValue.Key] = new StringValues(keyValue.Value.ToArray()); } using (var stream = await webMessage.Content.ReadAsStreamAsync()) using (var reader = new StreamReader(stream)) { webMessage.EnsureSuccessStatusCode(); text = reader.ReadToEnd(); } } catch (Exception e) { text = e.Message; } await response.WriteAsync(text); } } } else { await _next.Invoke(httpContext); } }
private bool IsValidForPrerenderPage(HttpRequest request, IHttpRequestFeature requestFeature) { var userAgent = request.Headers[Constants.HttpHeader_UserAgent]; var rawUrl = requestFeature.RawTarget; var relativeUrl = request.Path.ToString(); // check if follows google search engine suggestion if (request.Query.Keys.Any(a => a.Equals(Constants.EscapedFragment, StringComparison.OrdinalIgnoreCase))) return true; // check if has user agent if (string.IsNullOrEmpty(userAgent)) return false; // check if it's crawler user agent. var crawlerUserAgentPattern = Configuration.CrawlerUserAgentPattern ?? Constants.CrawlerUserAgentPattern; if (string.IsNullOrEmpty(crawlerUserAgentPattern) || !Regex.IsMatch(userAgent, crawlerUserAgentPattern, RegexOptions.IgnorePatternWhitespace)) return false; // check if the extenion matchs default extension if (Regex.IsMatch(relativeUrl, DefaultIgnoredExtensions, RegexOptions.IgnorePatternWhitespace)) return false; if (!string.IsNullOrEmpty(Configuration.AdditionalExtensionPattern) && Regex.IsMatch(relativeUrl, Configuration.AdditionalExtensionPattern, RegexOptions.IgnorePatternWhitespace)) return false; if (!string.IsNullOrEmpty(Configuration.BlackListPattern) && Regex.IsMatch(rawUrl, Configuration.BlackListPattern, RegexOptions.IgnorePatternWhitespace)) return false; if (!string.IsNullOrEmpty(Configuration.WhiteListPattern) && Regex.IsMatch(rawUrl, Configuration.WhiteListPattern, RegexOptions.IgnorePatternWhitespace)) return true; return false; }
In order to use PrerenderMiddleware in ASP.NET Core project easily, I have created some extension method, so that we can easily setup it in Startup.csbash
AddPrerenderConfig is used to add PrerenderConfiguration.json to IApplicationBuilder.
/// <summary> /// Add PrerenderConfiguration.json to configuration. /// Or you can put the configuration in appsettings.json file either. /// </summary> /// <param name="builder"></param> /// <param name="jsonFileName"></param> /// <returns></returns> public static IConfigurationBuilder AddPrerenderConfig(this IConfigurationBuilder builder, string jsonFileName = "PrerenderConfiguration.json") => builder.AddJsonFile(jsonFileName, false, true);
ConfigureSection is used to configure options into servicecollection, so that we can easily get it from servicecollection in the future.
/// <summary> /// Configure Section into Service Collections /// </summary> /// <typeparam name="TOptions"></typeparam> /// <param name="serviceCollection"></param> /// <param name="configuration"></param> /// <param name="singletonOptions"></param> public static void ConfigureSection<TOptions>(this IServiceCollection serviceCollection, IConfiguration configuration, bool singletonOptions = true) where TOptions : class, new() { serviceCollection.Configure<TOptions>(configuration.GetSection(typeof(TOptions).Name)); if (singletonOptions) { serviceCollection.AddSingleton<TOptions>(a => a.GetService<IOptions<TOptions>>().Value); } }
UsePrerender is used to register PrerenderMiddleware
#region UsePrerender /// <summary> /// Use Prerender Middleware to prerender JavaScript logic before turn back. /// </summary> /// <param name="app"></param> /// <param name="configuration">Prerender Configuration, if this parameter is NULL, will get the PrerenderConfiguration from ServiceCollection</param> /// <returns></returns> public static IApplicationBuilder UsePrerender(this IApplicationBuilder app, PrerenderConfiguration configuration = null) => app.UseMiddleware<PrerenderMiddleware>(configuration ?? app.ApplicationServices.GetService<IOptions<PrerenderConfiguration>>().Value); // => app.Use(next => new PrerenderMiddleware(next, configuration).Invoke); // => app.Use(next => context => new PrerenderMiddleware(next, configuration).Invoke(context)); // either way. #endregion
public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) // Prerender Step 1: Add Prerender configuration Json file. .AddPrerenderConfig() .AddEnvironmentVariables(); Configuration = builder.Build(); }
public void ConfigureServices(IServiceCollection services) { // Add framework services. services.AddMvc(); // Prerender Step 2: Add Options. services.AddOptions(); services.ConfigureSection<PrerenderConfiguration>(Configuration); }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); // Prerender Step 3: UsePrerender, before others. app.UsePrerender(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } else { app.UseExceptionHandler("/Home/Error"); } .............
I have added PrerenderConfiguration.json file into ASP.NET Core project, then I can configure for prerender service.
The format of this json file is:
{ "PrerenderConfiguration": { "ServiceUrl": "http://service.prerender.io", "Token": null, "CrawlerUserAgentPattern": null, "WhiteListPattern": null, "BlackListPattern": "lib|css|js", "AdditionalExtensionPattern": null, "ProxyUrl": null, "ProxyPort": 80 } }
You can go to my github wiki page to get more details about each option: Configuration & Check Priority
I have created a nuget package, which is very convenient if you don't want to dive deep into the source code.
Install Nuget Package in your project.
Visual Studio -> Tools -> Nuget Package Manager -> Package Manager Console.
Install-Package DotNetCoreOpen.PrerenderMiddleware
If you want to take a look more detail about this package, you can go https://www.nuget.org/packages/DotNetCoreOpen.PrerenderMiddleware/
Use PrerenderMiddleware and configure PrerenderConfiguration.json for prerender service.
I have fully documented how to do this in my github wiki page, you can go there take a look.
Done, try it out.
I also have created a github project to host all source code includes sample code for testing: https://github.com/dingyuliang/prerender-dotnet, in this project, it includes ASP.NET HttpModule, ASP.NET Core Middleware, IIS Configuration 3 different solution.
For ASP.NET Core Middleware, you can go to https://github.com/dingyuliang/prerender-dotnet/tree/master/src/DotNetCorePrerender
------------------------------------------------------------------------------------------------