Integrate SEO Audits into ASP.NET Core with SEOPeek API
ASP.NET Core is the go-to framework for building production .NET web applications, and it ships with everything you need to integrate external APIs cleanly: typed HttpClient factories, middleware pipelines, background services via IHostedService, and health checks with IHealthCheck. This guide walks you through building a complete SEO auditing layer into an ASP.NET Core application using the SEOPeek API. You will learn how to call the API and deserialize results into strongly-typed C# records, build middleware that audits pages automatically on publish, display audit results in Razor Pages, schedule recurring audits with a background service, and wire up health checks that verify your SEO score stays above a threshold—all without installing a single NuGet package beyond what ships with the framework.
1. Calling the SEOPeek API with HttpClient
The SEOPeek API is a single GET endpoint that accepts a URL and returns a comprehensive SEO audit. No API key is required for the free tier (50 audits per day), which makes it trivial to start integrating immediately. The endpoint:
GET https://us-central1-todd-agent-prod.cloudfunctions.net/seopeekApi/api/v1/audit?url=TARGET_URL
ASP.NET Core's IHttpClientFactory is the recommended way to manage outbound HTTP connections. It handles DNS rotation, connection pooling, and lifetime management automatically. Register a typed client in Program.cs:
// Program.cs
builder.Services.AddHttpClient<SeoAuditService>(client =>
{
client.BaseAddress = new Uri(
"https://us-central1-todd-agent-prod.cloudfunctions.net/seopeekApi/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(30);
});
Now create the service class. It wraps the HttpClient and provides a clean async method for auditing any URL. The GetFromJsonAsync extension method handles JSON deserialization in a single call—no manual stream reading or JsonSerializer.Deserialize needed.
using System.Net.Http.Json;
using System.Web;
public class SeoAuditService
{
private readonly HttpClient _http;
private readonly ILogger<SeoAuditService> _logger;
public SeoAuditService(HttpClient http, ILogger<SeoAuditService> logger)
{
_http = http;
_logger = logger;
}
public async Task<SeoAuditResult?> AuditAsync(
string targetUrl,
CancellationToken ct = default)
{
var encoded = HttpUtility.UrlEncode(targetUrl);
var requestUri = $"api/v1/audit?url={encoded}";
_logger.LogInformation("Auditing {Url}", targetUrl);
try
{
var result = await _http
.GetFromJsonAsync<SeoAuditResult>(requestUri, ct);
_logger.LogInformation(
"Audit complete: {Url} scored {Score} ({Grade})",
targetUrl, result?.Score, result?.Grade);
return result;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "SEOPeek API request failed for {Url}", targetUrl);
return null;
}
}
public async Task<List<SeoAuditResult>> AuditBatchAsync(
IEnumerable<string> urls,
int maxConcurrency = 5,
CancellationToken ct = default)
{
var results = new List<SeoAuditResult>();
var semaphore = new SemaphoreSlim(maxConcurrency);
var tasks = urls.Select(async url =>
{
await semaphore.WaitAsync(ct);
try
{
var result = await AuditAsync(url, ct);
if (result is not null)
{
lock (results) { results.Add(result); }
}
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
return results;
}
}
The AuditBatchAsync method uses a SemaphoreSlim to limit concurrency—the same pattern as Go's buffered channel semaphore, but expressed with .NET primitives. Set maxConcurrency to 5 for the free tier to avoid hitting rate limits, or increase it for paid plans.
Why IHttpClientFactory? Creating new HttpClient() per request causes socket exhaustion under load. The factory manages a pool of HttpMessageHandler instances with proper DNS TTL rotation. In ASP.NET Core, always use AddHttpClient for outbound API calls. It is the framework's built-in solution to a problem that has bitten every .NET developer at least once.
2. C# Response Models and Deserialization
The SEOPeek API returns a JSON object with a numeric score, a letter grade, and a dictionary of individual check results. C# records are ideal for modeling this response because they are immutable by default, generate value-based equality, and require minimal boilerplate.
using System.Text.Json.Serialization;
public record SeoAuditResult(
[property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("score")] int Score,
[property: JsonPropertyName("grade")] string Grade,
[property: JsonPropertyName("checks")] Dictionary<string, SeoCheck> Checks,
[property: JsonPropertyName("timestamp")] DateTime Timestamp
);
public record SeoCheck(
[property: JsonPropertyName("pass")] bool Pass,
[property: JsonPropertyName("message")] string Message,
[property: JsonPropertyName("value")] string? Value
);
Using records means you get ToString(), structural equality, and deconstruction for free. You can pattern-match on them, use them as dictionary keys, and serialize them back to JSON without any extra configuration.
For scenarios where you need mutable state (e.g., storing results in a database with Entity Framework), you can use a class instead:
public class SeoAuditEntity
{
public int Id { get; set; }
public string Url { get; set; } = string.Empty;
public int Score { get; set; }
public string Grade { get; set; } = string.Empty;
public DateTime AuditedAt { get; set; }
public List<SeoCheckEntity> Checks { get; set; } = new();
}
public class SeoCheckEntity
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public bool Pass { get; set; }
public string Message { get; set; } = string.Empty;
public string? Value { get; set; }
public int AuditId { get; set; }
public SeoAuditEntity Audit { get; set; } = null!;
}
Map between the API response records and your entity classes with a simple extension method. Keep the API models clean and let the persistence layer handle its own concerns.
3. ASP.NET Core Middleware for Publish-Time Audits
One of the most powerful patterns in ASP.NET Core is middleware—components that sit in the request pipeline and can inspect, modify, or react to every HTTP request. We can build middleware that automatically triggers an SEO audit whenever content is published through your CMS or admin panel.
The middleware watches for POST requests to specific publish routes (e.g., /admin/publish, /api/content). After the response completes successfully, it fires an asynchronous audit against the published URL. The audit runs in the background so it does not slow down the publish response.
public class SeoAuditMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SeoAuditMiddleware> _logger;
// Routes that trigger an audit after successful POST
private static readonly string[] PublishRoutes =
{
"/admin/publish",
"/api/content",
"/api/pages"
};
public SeoAuditMiddleware(
RequestDelegate next,
ILogger<SeoAuditMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(
HttpContext context,
SeoAuditService auditService)
{
// Let the request proceed through the pipeline
await _next(context);
// Only audit after successful POST to publish routes
if (context.Request.Method != HttpMethods.Post)
return;
if (context.Response.StatusCode is not (>= 200 and < 300))
return;
var path = context.Request.Path.Value?.ToLowerInvariant();
if (path is null || !PublishRoutes.Any(r => path.StartsWith(r)))
return;
// Extract the published URL from the request body or response
var publishedUrl = context.Items["PublishedUrl"] as string;
if (string.IsNullOrEmpty(publishedUrl))
return;
_logger.LogInformation(
"Content published at {Url}, triggering SEO audit", publishedUrl);
// Fire and forget — audit runs after response is sent
_ = Task.Run(async () =>
{
try
{
var result = await auditService.AuditAsync(publishedUrl);
if (result is not null && result.Score < 70)
{
_logger.LogWarning(
"SEO audit warning: {Url} scored {Score} ({Grade})",
publishedUrl, result.Score, result.Grade);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Background SEO audit failed for {Url}", publishedUrl);
}
});
}
}
// Extension method for clean registration
public static class SeoAuditMiddlewareExtensions
{
public static IApplicationBuilder UseSeoAuditOnPublish(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<SeoAuditMiddleware>();
}
}
Register the middleware in Program.cs after authentication and authorization:
app.UseAuthentication();
app.UseAuthorization();
app.UseSeoAuditOnPublish(); // Audit pages after publish
app.MapRazorPages();
The middleware uses Task.Run to offload the audit to a thread pool thread after the response has been sent. This is important—you do not want an API call to a third-party service blocking the publish response. The content editor sees a fast publish confirmation, and the SEO audit happens silently in the background. If the score drops below 70, a warning is logged so your team can investigate.
Production tip: For more robust background processing, consider queuing the audit URL to a Channel<string> that a background service reads from. This decouples the middleware from the HTTP call entirely and gives you retry logic, backpressure control, and graceful shutdown handling. The System.Threading.Channels API is built into .NET and is the recommended pattern for producer-consumer scenarios.
4. Displaying Audit Results in Razor Pages
Razor Pages gives you a clean model-view pattern for rendering SEO audit results in a dashboard. The page model calls the audit service, and the Razor template renders the results with color-coded scores and expandable check details.
Create the page model at Pages/SeoAudit.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
public class SeoAuditModel : PageModel
{
private readonly SeoAuditService _auditService;
public SeoAuditModel(SeoAuditService auditService)
{
_auditService = auditService;
}
[BindProperty(SupportsGet = true)]
public string? Url { get; set; }
public SeoAuditResult? AuditResult { get; private set; }
public string? ErrorMessage { get; private set; }
public async Task<IActionResult> OnGetAsync(CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(Url))
return Page();
AuditResult = await _auditService.AuditAsync(Url, ct);
if (AuditResult is null)
ErrorMessage = "Audit failed. Please check the URL and try again.";
return Page();
}
}
The Razor template at Pages/SeoAudit.cshtml renders the form and results:
@page
@model SeoAuditModel
@{
ViewData["Title"] = "SEO Audit";
}
<h1>SEO Audit</h1>
<form method="get">
<div class="input-group mb-3">
<input type="url" asp-for="Url" class="form-control"
placeholder="https://example.com" required />
<button type="submit" class="btn btn-primary">Audit</button>
</div>
</form>
@if (Model.ErrorMessage is not null)
{
<div class="alert alert-danger">@Model.ErrorMessage</div>
}
@if (Model.AuditResult is not null)
{
var result = Model.AuditResult;
var scoreClass = result.Score >= 80 ? "text-success"
: result.Score >= 50 ? "text-warning"
: "text-danger";
<div class="card mt-4">
<div class="card-body">
<h2 class="@scoreClass">
Score: @result.Score / 100
<span class="badge bg-secondary">@result.Grade</span>
</h2>
<p class="text-muted">@result.Url</p>
<table class="table table-sm mt-3">
<thead>
<tr>
<th>Check</th>
<th>Status</th>
<th>Details</th>
</tr>
</thead>
<tbody>
@foreach (var (name, check) in result.Checks)
{
<tr>
<td>@name</td>
<td>
@if (check.Pass)
{
<span class="badge bg-success">PASS</span>
}
else
{
<span class="badge bg-danger">FAIL</span>
}
</td>
<td>@check.Message</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
This gives your team a browser-based SEO audit tool built directly into your application. Enter a URL, hit audit, and see color-coded results with individual check breakdowns. The SupportsGet = true attribute on the Url property means audit results are shareable via URL—bookmark /SeoAudit?url=https://example.com and the audit runs automatically when the page loads.
5. Scheduled Audits with IHostedService
For continuous SEO monitoring, you want audits to run on a schedule without manual intervention. ASP.NET Core's BackgroundService base class (which implements IHostedService) is designed exactly for this. It runs inside the same process as your web application, starts when the app starts, and shuts down gracefully when the app stops.
public class ScheduledSeoAuditService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<ScheduledSeoAuditService> _logger;
private readonly IConfiguration _config;
public ScheduledSeoAuditService(
IServiceScopeFactory scopeFactory,
ILogger<ScheduledSeoAuditService> logger,
IConfiguration config)
{
_scopeFactory = scopeFactory;
_logger = logger;
_config = config;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Scheduled SEO audit service starting");
// Read the interval from configuration (default: 6 hours)
var intervalHours = _config.GetValue("SeoAudit:IntervalHours", 6);
using var timer = new PeriodicTimer(TimeSpan.FromHours(intervalHours));
// Run immediately on startup, then on schedule
await RunAuditCycleAsync(stoppingToken);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await RunAuditCycleAsync(stoppingToken);
}
}
private async Task RunAuditCycleAsync(CancellationToken ct)
{
_logger.LogInformation("Starting scheduled SEO audit cycle");
using var scope = _scopeFactory.CreateScope();
var auditService = scope.ServiceProvider
.GetRequiredService<SeoAuditService>();
// URLs to monitor — from config, database, or sitemap
var urls = _config.GetSection("SeoAudit:Urls")
.Get<string[]>() ?? Array.Empty<string>();
if (urls.Length == 0)
{
_logger.LogWarning("No URLs configured for SEO auditing");
return;
}
_logger.LogInformation(
"Auditing {Count} URLs", urls.Length);
var results = await auditService.AuditBatchAsync(
urls, maxConcurrency: 3, ct);
// Log summary
var avg = results.Count > 0
? results.Average(r => r.Score)
: 0;
_logger.LogInformation(
"Audit cycle complete: {Count} URLs, avg score {Avg:F1}",
results.Count, avg);
// Flag pages with poor scores
var poor = results.Where(r => r.Score < 60).ToList();
if (poor.Count > 0)
{
_logger.LogWarning(
"{Count} pages scored below 60: {Urls}",
poor.Count,
string.Join(", ", poor.Select(r => $"{r.Url} ({r.Score})")));
}
}
}
Register the background service in Program.cs:
builder.Services.AddHostedService<ScheduledSeoAuditService>();
And configure the URLs and interval in appsettings.json:
{
"SeoAudit": {
"IntervalHours": 6,
"Urls": [
"https://yoursite.com",
"https://yoursite.com/pricing",
"https://yoursite.com/features",
"https://yoursite.com/blog",
"https://yoursite.com/docs"
]
}
}
The PeriodicTimer class, introduced in .NET 6, is the modern replacement for Timer callbacks. It integrates with CancellationToken so the service shuts down cleanly when the application stops—no orphaned threads or unobserved exceptions.
Notice the use of IServiceScopeFactory instead of injecting SeoAuditService directly. Background services are singletons, but the typed HttpClient is scoped. Creating a scope per audit cycle ensures the HttpClient handler is properly rotated and disposed. This is a common gotcha in ASP.NET Core—always create a scope when resolving scoped services from a singleton.
Scaling tip: For the free tier (50 audits/day), limit your monitored URLs to 8 or fewer and audit every 6 hours (4 cycles x 8 URLs = 32 audits/day, well within limits). The Starter plan ($9/month, 1,000 audits) supports monitoring 40+ URLs every 6 hours comfortably. The Pro plan ($29/month, 10,000 audits) can handle 100+ URLs on an hourly schedule.
6. SEO Health Check with IHealthCheck
ASP.NET Core's health check system lets you expose a /healthz endpoint that load balancers, Kubernetes, and monitoring tools query to verify your application is healthy. We can extend this to include SEO health—if your homepage SEO score drops below a threshold, the health check reports degraded status.
using Microsoft.Extensions.Diagnostics.HealthChecks;
public class SeoHealthCheck : IHealthCheck
{
private readonly SeoAuditService _auditService;
private readonly IConfiguration _config;
public SeoHealthCheck(
SeoAuditService auditService,
IConfiguration config)
{
_auditService = auditService;
_config = config;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken ct = default)
{
var url = _config.GetValue<string>("SeoAudit:HealthCheckUrl")
?? _config.GetSection("SeoAudit:Urls").Get<string[]>()?.FirstOrDefault();
if (string.IsNullOrEmpty(url))
{
return HealthCheckResult.Healthy(
"No SEO health check URL configured");
}
try
{
var result = await _auditService.AuditAsync(url, ct);
if (result is null)
{
return HealthCheckResult.Degraded(
"SEO audit API unreachable");
}
var threshold = _config.GetValue("SeoAudit:HealthThreshold", 60);
var data = new Dictionary<string, object>
{
["url"] = result.Url,
["score"] = result.Score,
["grade"] = result.Grade,
["threshold"] = threshold,
["checksTotal"] = result.Checks.Count,
["checksPassed"] = result.Checks.Count(c => c.Value.Pass),
["checksFailed"] = result.Checks.Count(c => !c.Value.Pass)
};
if (result.Score >= threshold)
{
return HealthCheckResult.Healthy(
$"SEO score {result.Score} ({result.Grade}) above threshold {threshold}",
data);
}
return HealthCheckResult.Degraded(
$"SEO score {result.Score} ({result.Grade}) below threshold {threshold}",
data: data);
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"SEO audit health check failed", ex);
}
}
}
Register the health check in Program.cs:
builder.Services.AddHealthChecks()
.AddCheck<SeoHealthCheck>(
"seo-audit",
failureStatus: HealthStatus.Degraded,
tags: new[] { "seo", "monitoring" });
// Map the endpoint
app.MapHealthChecks("/healthz");
Now a GET request to /healthz runs your standard health checks plus the SEO audit. The response includes the score, grade, and check breakdown as structured data. Monitoring tools like Datadog, Grafana, or Azure Monitor can alert you when the SEO health check reports degraded status.
Configure the threshold in appsettings.json:
{
"SeoAudit": {
"HealthCheckUrl": "https://yoursite.com",
"HealthThreshold": 70
}
}
This turns SEO monitoring into a first-class operational concern. Your homepage SEO score sits alongside database connectivity, disk space, and memory usage in the same health check dashboard. When the score drops, you know about it before your search rankings suffer.
7. Pricing Comparison
For .NET developers building SEO monitoring into their applications, the cost of the auditing API matters. Here is how SEOPeek compares to the established players:
| Tool | Price | API Access | Free Tier | .NET SDK |
|---|---|---|---|---|
| SEOPeek | Free / $9 / $29 | REST API | 50/day, no key | HttpClient |
| SEOptimer | $29/mo | REST API | No | HttpClient |
| Seobility | $50/mo | REST API | Limited | HttpClient |
| Ahrefs | $99/mo | No audit API | No | N/A |
SEOPeek's free tier (50 audits/day, no API key) is uniquely generous for development and testing. You can build and test your entire integration without entering a credit card. The Starter plan at $9/month (1,000 audits) covers most small-to-medium sites with scheduled monitoring. The Pro plan at $29/month (10,000 audits) matches SEOptimer's price but delivers far more volume, and it costs less than half of Seobility's $50/month entry point.
For agencies running audits across multiple client sites, the Pro plan is the clear winner. At $29/month for 10,000 audits, you can monitor 50 client sites with 200 pages each on a daily schedule—something that would cost hundreds per month with competing tools.
SEOPeek plan breakdown
| Plan | Audits | Price | Best for |
|---|---|---|---|
| Free | 50 / day | $0 | Development, testing, small projects |
| Starter | 1,000 / month | $9/mo | Production monitoring, CI/CD gates |
| Pro | 10,000 / month | $29/mo | Agencies, multi-site monitoring, dashboards |
View full plan details and sign up on the SEOPeek pricing page.
Frequently Asked Questions
How do I call the SEOPeek API from ASP.NET Core?
Register a typed HttpClient in Program.cs using AddHttpClient<SeoAuditService>() with the SEOPeek base URL. The service uses GetFromJsonAsync to deserialize the JSON response directly into C# record types. The free tier (50 audits/day) requires no API key—just pass the target URL as a query parameter and you get back a complete audit result with score, grade, and individual check details. For paid plans, add the API key as an X-Api-Key header via DefaultRequestHeaders.
Can I run SEO audits automatically when content is published in ASP.NET Core?
Yes. Create an ASP.NET Core middleware component that intercepts POST requests to your publish routes. After the response completes successfully, the middleware fires an asynchronous audit against the published URL using Task.Run. The audit runs in the background so it does not slow down the publish response. Content editors get instant confirmation while the SEO audit happens silently. If the score drops below a configurable threshold, the middleware logs a warning that your team can monitor. For more robust processing, queue URLs to a Channel<string> and process them in a BackgroundService.
How do I schedule recurring SEO audits in ASP.NET Core?
Inherit from BackgroundService (which implements IHostedService) to create a long-running background task. Use .NET 6's PeriodicTimer to trigger audits on a configurable schedule. The background service reads URLs from appsettings.json or a database, audits each one via the SEOPeek API with bounded concurrency using SemaphoreSlim, and logs results. Register it with AddHostedService<ScheduledSeoAuditService>() and it runs inside the same ASP.NET Core process with no external scheduler, no cron jobs, and no Windows Task Scheduler needed.
Does the SEOPeek API require an API key for the free tier?
No. The free tier provides 50 audits per day with no API key at all. Send a GET request with the url query parameter and you receive a full audit response. For the Starter ($9/month) and Pro ($29/month) plans, you receive an API key to include as an X-Api-Key header for higher rate limits. The C# integration code in this guide supports both modes—just pass the API key through configuration and the service adds it to outbound requests automatically.
How does SEOPeek compare to SEOptimer and Ahrefs for API-based SEO auditing?
SEOPeek starts free with 50 audits per day and no API key. The Pro plan delivers 10,000 audits for $29/month. SEOptimer charges $29/month for significantly fewer API calls and requires authentication for every request. Seobility's API access starts at $50/month. Ahrefs does not offer a standalone audit API—their site audit is bundled into the $99/month subscription and is not callable programmatically. For .NET developers who want to embed SEO monitoring directly in their applications, SEOPeek offers the lowest barrier to entry, the most generous free tier, and a simple REST API that works with standard HttpClient without any proprietary SDK.
Add SEO Monitoring to Your .NET App
50 free audits per day. No API key required. Works with standard HttpClient—no NuGet packages needed.
See pricing plans for higher volumes.