Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Prerender.AspNetCore.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>Prerender.AspNetCore</PackageId>
<Version>1.0.0</Version>
<Authors>Prerender.io</Authors>
<Description>ASP.NET Core middleware for prerendering JavaScript-rendered pages for SEO via Prerender.io</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/prerender/integrations</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>
107 changes: 107 additions & 0 deletions PrerenderMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Prerender.AspNetCore;

public class PrerenderMiddleware : IMiddleware
{
private static readonly string[] CrawlerUserAgents =
[
"googlebot", "yahoo", "bingbot", "baiduspider",
"facebookexternalhit", "twitterbot", "rogerbot", "linkedinbot",
"embedly", "quora link preview", "showyoubot", "outbrain",
"pinterest", "slackbot", "w3c_validator", "perplexity",
"oai-searchbot", "chatgpt-user", "gptbot", "claudebot", "amazonbot",
];

private static readonly string[] ExtensionsToIgnore =
[
".js", ".css", ".xml", ".less", ".png", ".jpg", ".jpeg", ".gif",
".pdf", ".doc", ".txt", ".ico", ".rss", ".zip", ".mp3", ".rar",
".exe", ".wmv", ".avi", ".ppt", ".mpg", ".mpeg", ".tif", ".wav",
".mov", ".psd", ".ai", ".xls", ".mp4", ".m4a", ".swf", ".dat",
".dmg", ".iso", ".flv", ".m4v", ".torrent", ".ttf", ".woff", ".svg",
];

private readonly IHttpClientFactory _httpClientFactory;
private readonly PrerenderOptions _options;
private readonly ILogger<PrerenderMiddleware> _logger;

public PrerenderMiddleware(
IHttpClientFactory httpClientFactory,
IOptions<PrerenderOptions> options,
ILogger<PrerenderMiddleware> logger)
{
_httpClientFactory = httpClientFactory;
_options = options.Value;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!ShouldPrerender(context))
{
await next(context);
return;
}

try
{
var client = _httpClientFactory.CreateClient("prerender");
var apiUrl = BuildApiUrl(context);

using var request = new HttpRequestMessage(HttpMethod.Get, apiUrl);
request.Headers.TryAddWithoutValidation(
"User-Agent", context.Request.Headers["User-Agent"].ToString());
if (!string.IsNullOrWhiteSpace(_options.Token))
request.Headers.TryAddWithoutValidation("X-Prerender-Token", _options.Token);
request.Headers.TryAddWithoutValidation("X-Prerender-Int-Type", "AspNetCore");

using var response = await client.SendAsync(request, context.RequestAborted);
context.Response.StatusCode = (int)response.StatusCode;
var body = await response.Content.ReadAsStringAsync();
await context.Response.WriteAsync(body);
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "Prerender service unreachable, falling back");
await next(context);
}
}

private static bool ShouldPrerender(HttpContext context)
{
if (context.Request.Method != HttpMethods.Get) return false;

var path = context.Request.Path.Value ?? string.Empty;
if (IsStaticAsset(path)) return false;

if (context.Request.Query.ContainsKey("_escaped_fragment_")) return true;
if (context.Request.Headers.ContainsKey("X-Bufferbot")) return true;

var ua = context.Request.Headers["User-Agent"].ToString();
return !string.IsNullOrEmpty(ua) && IsBot(ua);
}

private string BuildApiUrl(HttpContext context)
{
var serviceUrl = _options.ServiceUrl.TrimEnd('/') + "/";
var scheme = context.Request.Scheme;
var host = context.Request.Host.Value;
var pathAndQuery = context.Request.Path + context.Request.QueryString;
return $"{serviceUrl}{scheme}://{host}{pathAndQuery}";
}

private static bool IsBot(string userAgent)
{
var ua = userAgent.ToLowerInvariant();
return CrawlerUserAgents.Any(bot => ua.Contains(bot));
}

private static bool IsStaticAsset(string path)
{
var lower = path.ToLowerInvariant();
return ExtensionsToIgnore.Any(ext => lower.EndsWith(ext));
}
}
7 changes: 7 additions & 0 deletions PrerenderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Prerender.AspNetCore;

public class PrerenderOptions
{
public string? Token { get; set; }
public string ServiceUrl { get; set; } = "https://service.prerender.io/";
}
18 changes: 18 additions & 0 deletions PrerenderServiceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace Prerender.AspNetCore;

public static class PrerenderServiceExtensions
{
public static IServiceCollection AddPrerender(this IServiceCollection services)
{
services.AddOptions<PrerenderOptions>().BindConfiguration("Prerender");
services.AddHttpClient("prerender");
services.AddTransient<PrerenderMiddleware>();
return services;
}

public static IApplicationBuilder UsePrerender(this IApplicationBuilder app)
=> app.UseMiddleware<PrerenderMiddleware>();
}
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# prerender-aspnetcore

ASP.NET Core middleware for [Prerender.io](https://prerender.io). Intercepts requests from bots and crawlers and serves prerendered HTML, so your JavaScript-rendered app is fully indexable by search engines and social media scrapers.

Compatible with **ASP.NET Core 8+** and **.NET 8+**.

## Installation

```bash
dotnet add package Prerender.AspNetCore
```

## Setup

Register the middleware in `Program.cs`:

```csharp
builder.Services.AddPrerender();

var app = builder.Build();
app.UsePrerender(); // place before routing middleware
```

Add your token to `appsettings.json`:

```json
{
"Prerender": {
"Token": "YOUR_PRERENDER_TOKEN"
}
}
```

The middleware must be placed **before** routing to intercept bot requests early.

## Settings

| Setting | Default | Description |
|---------|---------|-------------|
| `Prerender:Token` | `null` | Your Prerender.io token |
| `Prerender:ServiceUrl` | `https://service.prerender.io/` | Prerender service URL (override for self-hosted Prerender) |

## Self-hosted Prerender

```json
{
"Prerender": {
"ServiceUrl": "http://your-prerender-server:3000"
}
}
```

## How it works

Requests are prerendered when **all** of the following are true:

- The HTTP method is `GET`
- The `User-Agent` matches a known bot/crawler (Googlebot, Bingbot, Twitterbot, GPTBot, ClaudeBot, etc.)
— OR the URL contains `_escaped_fragment_`
— OR the `X-Bufferbot` header is present
- The URL does not end with a static asset extension (`.js`, `.css`, `.png`, etc.)

Everything else passes through to your normal ASP.NET Core pipeline.

If the Prerender service is unreachable, the middleware falls back gracefully and serves the normal response.

## License

MIT
17 changes: 17 additions & 0 deletions tests/Prerender.AspNetCore.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Prerender.AspNetCore.csproj" />
</ItemGroup>
</Project>
Loading