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">
© 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 libraries | Local libraries |
|---|---|
| Require access to an external host | Served by the application |
| Simple for quick demonstrations | Predictable project-controlled versions |
| May benefit from shared caching | Work in isolated development environments |
| External URL appears in layout | Files 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
| Statement | Responsibility |
|---|---|
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:
| Test | Expected result |
|---|---|
/ | Bootstrap-styled Home page with server time |
/privacy | Privacy Policy page |
/?handler=Time | Plain server-time response |
/contact | Custom page with HTTP 404 status |
| Narrow browser window | Responsive navigation button |
| Disconnect after initial library installation | Bootstrap 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
| Part | Milestone |
|---|---|
| 1 | Minimal ASP.NET Core text response |
| 2 | HTML response and dynamic value |
| 3 | Home, Privacy and 404 routes |
| 4 | CSS and static files |
| 5 | Razor Pages |
| 6 | Shared layout |
| 7 | Page Models |
| 8 | Named handler and asynchronous request |
| 9 | Bootstrap and Tag Helpers |
| 10 | Local 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.