ASP.NET Core SEO: Redirects, HTTPS and Headers on IIS and Kestrel

Crawvix Editorial Team · Published October 29, 2026 · Last updated October 29, 2026 · 8 min read

ASP.NET Core sites often look correct in a browser and still send search engines mixed signals. The HTTP address redirects, but with a temporary status code. The www and non-www hosts both answer with 200. Behind IIS or a proxy, the app believes every request is plain HTTP and redirects in a loop. A missing page shows a friendly message but returns a success code.

This guide is for developers and site owners who run an ASP.NET Core site (Razor Pages, MVC or minimal APIs) and want clean, crawlable responses. It covers the pipeline order, one permanent HTTP-to-HTTPS redirect, one canonical host, safe HSTS, legacy URL redirects and real 404 status codes. You will finish with a short curl test that proves each response.

These settings remove technical problems. They do not make a page rank, and they do not speed up indexing.

Before you begin

  • Access: source code, the ability to deploy, and (on IIS) access to the site's web.config. You also need to know your hosting setup: Kestrel exposed directly, IIS in front of Kestrel, or another reverse proxy such as nginx.
  • Decide the canonical address: for example https://www.example.com. Every other variant should redirect to it. Our guide on www vs non-www helps you pick.
  • Risk: moderate. A wrong redirect rule can loop and take the site down. A long HSTS lifetime is hard to undo. Test on a staging site first.
  • Back up: commit your code and copy the live web.config and appsettings.json before you edit.
  • Tools: curl or any tool that shows response status lines and headers.

How the pieces fit together

Three layers can redirect: your application, the web server (IIS or nginx) and a CDN or proxy in front. Microsoft's documentation says that if the proxy handles HTTPS or redirection, the app does not need its own HTTPS redirection middleware. The problem starts when two layers both redirect, or when the app cannot see the original scheme. Pick one layer for each redirect and test it.

Middleware order matters. Forwarded headers must run before HTTPS redirection and HSTS, and URL rewriting should run early, before static files and routing.

Step-by-step instructions

Step 1: Make the app see the real scheme

Why: behind a reverse proxy, the connection to your app may be plain HTTP even when the visitor used HTTPS. Without the original scheme the app redirects again and again, which browsers report as "too many redirects".

Action: call UseForwardedHeaders first. The proxy must send X-Forwarded-Proto.

builder.Services.Configure<ForwardedHeadersOptions>(o =>
{
    o.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    // Trust only your proxy. Add its address here.
    o.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});

var app = builder.Build();
app.UseForwardedHeaders();

Microsoft documents that, when ASP.NET Core runs out-of-process behind IIS, the IIS integration enables a restricted forwarded-headers setup automatically for a single localhost proxy. Outside that case it is off by default, and by default only loopback addresses are trusted. Do not trust unknown networks, because forwarded headers can be forged.

Confirm: log Request.Scheme for a request made over HTTPS. It must say https.

Step 2: Redirect HTTP to HTTPS once, and permanently

Why: search engines need to learn that the HTTPS URL is the lasting address. The default UseHttpsRedirection status is 307, a temporary redirect. Google treats 301 and 308 as permanent and 302, 303 and 307 as temporary, and a temporary redirect does not send the same signal that the target should be canonical.

Action: set a permanent status for non-development environments and tell the middleware the HTTPS port.

using static Microsoft.AspNetCore.Http.StatusCodes;

if (!builder.Environment.IsDevelopment())
{
    builder.Services.AddHttpsRedirection(o =>
    {
        o.RedirectStatusCode = Status308PermanentRedirect;
        o.HttpsPort = 443;
    });
}

// after UseForwardedHeaders
app.UseHttpsRedirection();

If the middleware cannot find an HTTPS port, it does not redirect and logs "Failed to determine the https port for redirect." Setting HttpsPort, or the https_port setting, avoids that. Port discovery from ASPNETCORE_URLS does not work behind a reverse proxy.

On IIS you can do this redirect in web.config instead. The URL Rewrite module supports status codes 301, 302, 303 and 307 for redirects, and Microsoft notes that 308 is not supported there.

<rewrite>
  <rules>
    <rule name="HTTP to HTTPS" stopProcessing="true">
      <match url="(.*)" />
      <conditions>
        <add input="{HTTPS}" pattern="off" />
      </conditions>
      <action type="Redirect" url="https://www.example.com/{R:1}" redirectType="Permanent" />
    </rule>
  </rules>
</rewrite>

Use this or the middleware, not both.

Confirm: curl -sI http://www.example.com/page returns 301 or 308 and a Location header with the https:// address.

Step 3: Choose one canonical host

Why: if example.com and www.example.com both return 200, search engines see two copies of the site.

Action: redirect the non-canonical host. The URL Rewriting middleware has helpers: AddRedirectToWwwPermanent() sends a 308 from non-www to www. The documentation lists no non-www helper, so for the other direction write a regular expression rule or a custom rule. On IIS, add a rule that tests {HTTP_HOST} and redirects with redirectType="Permanent".

var rewrite = new RewriteOptions().AddRedirectToWwwPermanent();
app.UseRewriter(rewrite);

Combine this with Step 2 carefully. A visitor who requests http://example.com/page should reach https://www.example.com/page in one hop where you can manage it. See redirect chains and loops if you see more than one.

Confirm: all four variants (http/https with and without www) end at the canonical URL, each with a single permanent redirect where possible.

Step 4: Add HSTS carefully

Why: HSTS tells browsers to use HTTPS for your domain without trying HTTP first. It is a browser feature. It is not a ranking signal, and it cannot fix a bad redirect.

Action: enable it only outside development, and start with a short lifetime.

builder.Services.AddHsts(o =>
{
    o.MaxAge = TimeSpan.FromHours(1);   // raise only after HTTPS is proven stable
    o.IncludeSubDomains = false;
});

if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}

The default max-age is 30 days. Microsoft advises starting small, at most a day, for a first rollout, and raising it (commonly to a year) once HTTPS is stable. Skip UseHsts if your proxy already sends the header, such as native HSTS in IIS 10.0 version 1709 or later. Never use it in development, because browsers cache the setting. For the full rollout, read HSTS setup.

Confirm: curl -sI https://www.example.com/ shows one Strict-Transport-Security header.

Step 5: Redirect moved URLs with explicit status codes

Why: after a redesign or rename, old URLs should lead to the new ones so visitors and crawlers do not hit errors.

Action: AddRedirect defaults to 302, which is temporary. Pass 301 for moves that will not be reversed.

var rewrite = new RewriteOptions()
    .AddRedirect("old-blog/(.*)", "/blog/$1", StatusCodes.Status301MovedPermanently);
app.UseRewriter(rewrite);

Use a fixed local prefix in the destination (/blog/$1), because a destination built only from $1 can supply a full address and become an open redirect. Place UseRewriter before static files and routing. If you protect pages with authorization, add authentication and authorization after the rewriter. Read 301 vs 302 redirects for choosing codes.

Confirm: curl -sI https://www.example.com/old-blog/hello shows 301 and Location: /blog/hello, and the target returns 200.

Step 6: Return real 404 responses

Why: a custom "not found" page that answers with 200 is a soft 404. A missing page should answer with a 404 status.

Action: use UseStatusCodePagesWithReExecute. It re-runs the pipeline on an error path and keeps the original status code, so a 404 stays a 404. UseStatusCodePagesWithRedirects sends a 302 to an error page that typically returns 200, which is the wrong pattern for missing pages.

app.UseExceptionHandler("/Error");
app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

In the error page handler, do not set the status to 200. Also return NotFound() from controllers or pages when a record is missing, rather than rendering an empty page. More in soft 404 errors.

Confirm: curl -sI https://www.example.com/no-such-page returns 404 and the browser shows your designed page.

Step 7: Run the whole test

Run these against production-like settings:

curl -sI http://example.com/
curl -sI http://www.example.com/
curl -sI https://example.com/
curl -sI https://www.example.com/
curl -sI https://www.example.com/old-blog/hello
curl -sI https://www.example.com/no-such-page

Check status, Location and Strict-Transport-Security on each. See HTTP security headers for related headers.

Final verification checklist

  • Request.Scheme is https for HTTPS visitors, including behind a proxy.
  • Each HTTP URL redirects to its HTTPS canonical address with a 301 or 308.
  • Only one canonical host answers with 200.
  • No redirect chain has more than one hop where avoidable, and none loops.
  • HSTS is sent once, only over HTTPS, with a lifetime you chose on purpose.
  • Moved URLs return 301 and land on a 200 page.
  • Missing URLs return 404, not 200 or a redirect to the home page.
  • IIS and application rules do not both redirect the same request.

Common mistakes to avoid

  • Leaving the default 307. Set a permanent status for the HTTPS redirect once the site is stable.
  • Redirecting in both IIS and the app. Choose one layer, or you will get chains and loops.
  • Skipping forwarded headers behind a proxy. The app sees HTTP and redirects forever.
  • Trusting every proxy. Limit KnownProxies or KnownNetworks to addresses you control.
  • Starting HSTS at a year. Begin short, then raise it.
  • Using $1 alone as a redirect destination. Add a fixed local prefix.
  • Redirecting missing pages to the home page. Return a 404 so the status is honest.
  • Using 308 in an IIS rewrite rule. The module does not support it; use 301.

Frequently asked questions

Is a 307 redirect bad for SEO?

It is a temporary redirect. Google follows it but does not treat the target as the canonical signal in the same way as a permanent one. For a redirect you intend to keep, use 301 or 308.

Should I use IIS URL Rewrite or the ASP.NET Core middleware?

Microsoft recommends the IIS module on IIS when you need features the middleware lacks, such as file and directory checks. The middleware suits HTTP.sys or cases without server modules. Whatever you pick, keep the rules in one place.

Do I need HSTS for SEO?

No. It improves HTTPS enforcement for returning visitors. Search engines still depend on correct redirects.

Does Kestrel need to listen on port 80?

When Kestrel is the public-facing server, it must listen on both the secure and insecure ports so clients can reach the HTTP URL and be redirected.

Official references


Next step: See how Crawvix checks a website and capture a baseline once scanning opens. This guide provides technical information, not a guarantee of rankings, traffic or index inclusion.

In this category

All 9 Platforms guides