In the previous post Use Prerender to improve AngularJS SEO, I have explained different solutions at 3 different levels to implement Prerender. In this post, I will explain how to implement a ASP.NET HttpModule as a application level middleware to implement prerender. Since we call it ASP.NET, so it is applicable for both ASP.NET WebForm and MVC.javascript
At first, let's review what's the appliaction level middleware solution architecture.css
From above diagram, the easest way to implement it in ASP.NET is to create a HttpModule. Here I named it as PrerenderHttpModule.html
At first, let's create PrerenderHttpModule and register BeginRequest event.java
#region Implement IHttpModule /// <summary> /// init /// </summary> /// <param name="context"></param> public void Init(HttpApplication context) { context.BeginRequest += context_BeginRequest; } /// <summary> /// dispose /// </summary> public void Dispose() { } #endregion #region Begin Request protected void context_BeginRequest(object sender, EventArgs e) { try { Prerender(sender as HttpApplication); } catch (Exception exception) { Debug.Write(exception.ToString()); } } #endregion
In PrerenderHttpModule, the major method is Prerender(HttpApplication)git
private void Prerender(HttpApplication application) { var httpContext = application.Context; var request = httpContext.Request; var response = httpContext.Response; if (IsValidForPrerenderPage(request)) { // generate URL var requestUrl = request.Url.AbsoluteUri; // if traffic is forwarded from https://, we convert http:// to https://. if (string.Equals(request.Headers[Constants.HttpHeader_XForwardedProto], Constants.HttpsProtocol, StringComparison.InvariantCultureIgnoreCase) && requestUrl.StartsWith(Constants.HttpProtocol, StringComparison.InvariantCultureIgnoreCase)) { requestUrl = Constants.HttpsProtocol + requestUrl.Substring(Constants.HttpProtocol.Length); } var prerenderUrl = $"{Configuration.ServiceUrl.Trim('/')}/{requestUrl}"; // create request var webRequest = (HttpWebRequest)WebRequest.Create(prerenderUrl); webRequest.Method = "GET"; webRequest.UserAgent = request.UserAgent; webRequest.AllowAutoRedirect = false; webRequest.Headers.Add("Cache-Control", "no-cache"); webRequest.ContentType = "text/html"; // Proxy Information if (!string.IsNullOrEmpty(Configuration.ProxyUrl) && Configuration.ProxyPort > 0) webRequest.Proxy = new WebProxy(Configuration.ProxyUrl, Configuration.ProxyPort); // Add token if (!string.IsNullOrEmpty(Configuration.Token)) webRequest.Headers.Add(Constants.HttpHeader_XPrerenderToken, Configuration.Token); var webResponse = default(HttpWebResponse); try { // Get the web response and read content etc. if successful webResponse = (HttpWebResponse)webRequest.GetResponse(); } catch (WebException e) { // Handle response WebExceptions for invalid renders (404s, 504s etc.) - but we still want the content webResponse = e.Response as HttpWebResponse; } // write response response.StatusCode = (int)webResponse.StatusCode; foreach (string key in webResponse.Headers.Keys) { response.Headers[key] = webResponse.Headers[key]; } using (var reader = new StreamReader(webResponse.GetResponseStream(), DefaultEncoding)) { response.Write(reader.ReadToEnd()); } response.Flush(); application.CompleteRequest(); } }
Also, in order to make the logic flexible and easy to configure, I have add a configuration class and IsValidForPrerenderPage() methodangularjs
private bool IsValidForPrerenderPage(HttpRequest request) { var userAgent = request.UserAgent; var url = request.Url; var rawUrl = request.RawUrl; var relativeUrl = request.AppRelativeCurrentExecutionFilePath; // check if follows google search engine suggestion if (request.QueryString.AllKeys.Any(a => a.Equals(Constants.EscapedFragment, StringComparison.InvariantCultureIgnoreCase))) 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; }
The priority of checking whether is valid for prerender page:github
User Agent: Setting -> CrawlerUserAgentPatternweb
(google)|(bing)|(Slurp)|(DuckDuckBot)|(YandexBot)|(baiduspider)|(Sogou)|(Exabot)|(ia_archiver)|(facebot)|(facebook)|(twitterbot)|(rogerbot)|(linkedinbot)|(embedly)|(quora)|(pinterest)|(slackbot)|(redditbot)|(Applebot)|(WhatsApp)|(flipboard)|(tumblr)|(bitlybot)|(Discordbot)
\\.vxml|js|css|less|png|jpg|jpeg|gif|pdf|doc|txt|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent
Generally, we have two different ways to register a HttpModule in ASP.NET, bash
Here, I have added the logic to support both, and I have added a app setting to control which one we want to use. By default, it will use DynamicModuleUtility, as we don't need to configure HttpModule in web.config, it's automatical.app
Note, in order to use this HttpModule, please configure your IIS Application Pool to integrated mode.
<!--If it's false, please configure http module for UsePrestartForPrenderModule--> <add key="UsePrestartForPrenderModule" value="true"/>
This is the default configuration.
<!--If it's false, please configure http module for UsePrestartForPrenderModule--> <add key="UsePrestartForPrenderModule" value="true"/>
public static class PrerenderPreApplicationStart { public const string StartMethodName = "Prestart"; static bool UsePrestart = !bool.FalseString.Equals(ConfigurationManager.AppSettings[Constants.AppSetting_UsePrestartForPrenderModule], StringComparison.InvariantCultureIgnoreCase); /// <summary> /// used to configure for PreApplicationStart. /// i.e. [assembly: PreApplicationStartMethod(typeof(PrerenderPreApplicationStart), "Start")] /// </summary> public static void Prestart() { if (UsePrestart) { DynamicModuleUtility.RegisterModule(typeof(PrerenderHttpModule)); } } }
[assembly: PreApplicationStartMethodAttribute(typeof(PrerenderPreApplicationStart), PrerenderPreApplicationStart.StartMethodName)]
Once the ASP.NET application loads this assembly, it will trigger PrerenderPreApplicationStart.Prestart() method, then registers the PrerenderHttpModule.
This is a very common and easy way, what we need to do is to:
<!--If it's false, please configure http module for UsePrestartForPrenderModule--> <add key="UsePrestartForPrenderModule" value="false"/>
<system.webServer> <validation validateIntegratedModeConfiguration="false"/> <modules runAllManagedModulesForAllRequests="false"> <!--Configure PrerenderHttpModule when UsePrestartForPrenderModule is false; --> <add name="prerender" type="DotNetOpen.PrerenderModule.PrerenderHttpModule, DotNetOpen.PrerenderModule" /> <remove name="FormsAuthentication"/> </modules> </system.webServer>
I have added a configuration section PrerenderConfigurationSection for prerender options
<configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="prerender" type="DotNetOpen.PrerenderModule.Configuration.PrerenderConfigurationSection, DotNetOpen.PrerenderModule"/> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false"/> </configSections>
<!--prerender settings--> <!--CrawlerUserAgentPattern: "(google)|(bing)|(Slurp)|(DuckDuckBot)|(YandexBot)|(baiduspider)|(Sogou)|(Exabot)|(ia_archiver)|(facebot)|(facebook)|(twitterbot)|(rogerbot)|(linkedinbot)|(embedly)|(quora)|(pinterest)|(slackbot)|(redditbot)|(Applebot)|(WhatsApp)|(flipboard)|(tumblr)|(bitlybot)|(Discordbot)"--> <!--WhiteListPattern, BlackListPattern: will check raw URL, which includes query string--> <!--AdditionalExtensionPattern: will only check extension--> <prerender ServiceUrl="http://localhost:3000" Token="" WhiteListPattern="" BlackListPattern="" AdditionalExtensionPattern="" ProxyUrl="" 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 DotNetOpen.PrerenderModule
If you want to take a look more detail about this package, you can go https://www.nuget.org/packages/DotNetOpen.PrerenderModule/
There are several versions
Register PrerenderHttpModule and configure options 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 HttpModule, you can go to https://github.com/dingyuliang/prerender-dotnet/tree/master/src/DotNetPrerender
------------------------------------------------------------------------------------------------