ASP.NET Core 8 Website - Standard Razor Pages Project

PART 10: Completing the Standard ASP.NET Core 8 Razor Pages Project

PART 9 reproduced the default appearance using Bootstrap from a CDN. This final part stores the client libraries inside the project, adds the remaining default template files and completes the production request pipeline.

1. Install the Library Manager command-line tool

Run this once on the development computer:

dotnet tool install -g Microsoft.Web.LibraryManager.Cli

If it is already installed, update it with:

dotnet tool update -g Microsoft.Web.LibraryManager.Cli

Close and reopen the terminal if the libman command is not recognised immediately.

2. Install the client libraries locally

Run these commands from the basicdotnet project directory:

libman install bootstrap@5.3.3 \
  --provider jsdelivr \
  --destination wwwroot/lib/bootstrap

libman install jquery@3.7.1 \
  --provider jsdelivr \
  --destination wwwroot/lib/jquery

libman install jquery-validation@1.19.5 \
  --provider jsdelivr \
  --destination wwwroot/lib/jquery-validation

libman install jquery-validation-unobtrusive@4.0.0 \
  --provider jsdelivr \
  --destination wwwroot/lib/jquery-validation-unobtrusive

LibMan downloads the browser libraries and creates or updates libman.json. Unlike the CDN links used in PART 9, the application can now serve these files from wwwroot.

3. Restore libraries on another computer

After copying or cloning the project, restore the libraries recorded in libman.json with:

libman restore

The exact files installed by a package can vary by provider and package version. Confirm that the referenced dist paths exist after installation.

4. Replace the CDN references in _Layout.cshtml

Use this complete Pages/Shared/_Layout.cshtml:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0" />

    <title>@ViewData["Title"] - basicdotnet</title>

    <link rel="stylesheet"
          href="~/lib/bootstrap/dist/css/bootstrap.min.css" />

    <link rel="stylesheet"
          href="~/css/site.css"
          asp-append-version="true" />

    <link rel="stylesheet"
          href="~/basicdotnet.styles.css"
          asp-append-version="true" />
</head>

<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm
                    navbar-light bg-white border-bottom
                    box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand"
                   asp-page="/Index">
                    basicdotnet
                </a>

                <button class="navbar-toggler"
                        type="button"
                        data-bs-toggle="collapse"
                        data-bs-target=".navbar-collapse"
                        aria-controls="navbarSupportedContent"
                        aria-expanded="false"
                        aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>

                <div class="navbar-collapse collapse
                            d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark"
                               asp-page="/Index">
                                Home
                            </a>
                        </li>

                        <li class="nav-item">
                            <a class="nav-link text-dark"
                               asp-page="/Privacy">
                                Privacy
                            </a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>

    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2026 - basicdotnet -
            <a asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.min.js"></script>

    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>

    <script src="~/js/site.js"
            asp-append-version="true"></script>

    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

5. Why use local client libraries?

CDN librariesLocal libraries
Require access to an external hostServed by the application
Simple for quick demonstrationsPredictable project-controlled versions
May benefit from shared cachingWork in isolated development environments
External URL appears in layoutFiles appear under wwwroot/lib

6. Create wwwroot/js/site.js

// Place site-wide JavaScript in this file.

The file can remain nearly empty. Its presence matches the default template and provides a standard location for shared JavaScript.

7. Create the validation partial

Create Pages/Shared/_ValidationScriptsPartial.cshtml:

<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

This partial is not needed by the current Home and Privacy pages, but form pages can render it through their Scripts section:

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

8. Create scoped layout CSS

Create Pages/Shared/_Layout.cshtml.css:

a.navbar-brand {
  white-space: normal;
  text-align: center;
  word-break: break-all;
}

a {
  color: #0077cc;
}

.border-top {
  border-top: 1px solid #e5e5e5;
}

.border-bottom {
  border-bottom: 1px solid #e5e5e5;
}

.box-shadow {
  box-shadow: 0 0.25rem 0.75rem
    rgba(0, 0, 0, 0.05);
}

.footer {
  position: absolute;
  bottom: 0;
  width: 100%;
  white-space: nowrap;
  line-height: 60px;
}

At build time, ASP.NET Core bundles scoped CSS and exposes it through:

basicdotnet.styles.css

9. Use the standard site.css

Replace wwwroot/css/site.css with:

html {
  font-size: 14px;
}

@media (min-width: 768px) {
  html {
    font-size: 16px;
  }
}

.btn:focus,
.btn:active:focus,
.btn-link.nav-link:focus,
.form-control:focus,
.form-check-input:focus {
  box-shadow:
    0 0 0 0.1rem white,
    0 0 0 0.25rem #258cfb;
}

html {
  position: relative;
  min-height: 100%;
}

body {
  margin-bottom: 60px;
}

.error-code {
  color: #b02a37;
}

10. Add the standard application error page

The existing NotFound page handles unknown URLs. A separate Error page handles unexpected application exceptions.

Create Pages/Error.cshtml.cs:

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace basicdotnet.Pages;

[ResponseCache(
    Duration = 0,
    Location = ResponseCacheLocation.None,
    NoStore = true)]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId =>
        !string.IsNullOrEmpty(RequestId);

    public void OnGet()
    {
        RequestId = Activity.Current?.Id
            ?? HttpContext.TraceIdentifier;
    }
}

Create Pages/Error.cshtml:

@page
@model ErrorModel

@{
    ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">
    An error occurred while processing your request.
</h2>

@if (Model.ShowRequestId)
{
    <p>
        <strong>Request ID:</strong>
        <code>@Model.RequestId</code>
    </p>
}

11. Complete Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthorization();

app.MapRazorPages();
app.MapFallbackToPage("/NotFound");

app.Run();

12. Understand the completed request pipeline

StatementResponsibility
UseExceptionHandler("/Error")Uses the Error page outside development
UseHsts()Encourages HTTPS in production
UseHttpsRedirection()Redirects HTTP requests to HTTPS
UseStaticFiles()Serves CSS, JavaScript and libraries
UseRouting()Matches requests to endpoints
UseAuthorization()Enables endpoint authorization processing
MapRazorPages()Maps Razor Pages
MapFallbackToPage()Displays the custom 404 page

13. Check the completed structure

basicdotnet/
├── Pages/
│   ├── Shared/
│   │   ├── _Layout.cshtml
│   │   ├── _Layout.cshtml.css
│   │   └── _ValidationScriptsPartial.cshtml
│   ├── _ViewImports.cshtml
│   ├── _ViewStart.cshtml
│   ├── Error.cshtml
│   ├── Error.cshtml.cs
│   ├── Index.cshtml
│   ├── Index.cshtml.cs
│   ├── NotFound.cshtml
│   ├── NotFound.cshtml.cs
│   ├── Privacy.cshtml
│   └── Privacy.cshtml.cs
├── wwwroot/
│   ├── css/site.css
│   ├── js/site.js
│   └── lib/
│       ├── bootstrap/
│       ├── jquery/
│       ├── jquery-validation/
│       └── jquery-validation-unobtrusive/
├── appsettings.Development.json
├── appsettings.json
├── basicdotnet.csproj
├── libman.json
└── Program.cs

14. Build and test

libman restore
dotnet restore
dotnet build
dotnet run

Test the following:

TestExpected result
/Bootstrap-styled Home page with server time
/privacyPrivacy Policy page
/?handler=TimePlain server-time response
/contactCustom page with HTTP 404 status
Narrow browser windowResponsive navigation button
Disconnect after initial library installationBootstrap still loads locally

15. Compare with creating the final project directly

The complete Razor Pages foundation could have been generated immediately with:

dotnet new webapp -n basicdotnet --framework net8.0

That command is appropriate when the objective is to start building an application quickly. This ten-part progression used the smaller web template first so that every major component could be introduced and understood individually.

Final learning path

PartMilestone
1Minimal ASP.NET Core text response
2HTML response and dynamic value
3Home, Privacy and 404 routes
4CSS and static files
5Razor Pages
6Shared layout
7Page Models
8Named handler and asynchronous request
9Bootstrap and Tag Helpers
10Local libraries and standard project structure

Series complete: The project has progressed from a single plain-text endpoint to a structured ASP.NET Core 8 Razor Pages application with Home, Privacy, shared layout, Page Models, local Bootstrap assets, server-time handler, production error handling and a custom 404 page.

Official references