ASP.NET Core 8 Website - Bootstrap and Razor Tag Helpers
PART 9: Adding Bootstrap and Razor Tag Helpers
PART 8 completed the server-time behaviour. This part replaces the custom navigation with a responsive Bootstrap navigation bar and replaces hard-coded page URLs with ASP.NET Core Tag Helpers.
1. What will change?
| Before | After |
|---|---|
| Custom navigation classes | Bootstrap navbar classes |
href="/privacy" | asp-page="/Privacy" |
| Custom mobile wrapping | Bootstrap collapsible navigation |
| Script inside page content | Optional Razor Scripts section |
| No footer | Shared Bootstrap-styled footer |
2. Confirm that Tag Helpers are enabled
Pages/_ViewImports.cshtml should contain:
@using basicdotnet
@namespace basicdotnet.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
The final line enables attributes such as asp-page and asp-append-version.
3. Replace 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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />
<link rel="stylesheet"
href="~/css/site.css"
asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
This stage uses a pinned Bootstrap CDN address to keep the exercise small. A later part will replace it with local files under wwwroot/lib, matching the default Razor Pages project structure.
4. Understand asp-page
Previously, links were written directly:
<a href="/privacy">Privacy</a>
They now identify the Razor Page:
<a asp-page="/Privacy">Privacy</a>
ASP.NET Core generates the correct href when the page is rendered. This keeps navigation connected to the Razor Pages routing system.
5. Understand asp-append-version
<link rel="stylesheet"
href="~/css/site.css"
asp-append-version="true" />
The tilde represents the application web root. The Tag Helper appends a content-based version query to the generated URL, helping browsers retrieve a changed stylesheet instead of using a stale cached copy.
6. Understand the Bootstrap navigation
The main Bootstrap classes have the following roles:
| Class | Purpose |
|---|---|
navbar | Creates the navigation component |
navbar-expand-sm | Expands navigation on small and larger screens |
navbar-toggler | Creates the mobile menu button |
collapse navbar-collapse | Controls the collapsible link area |
container | Centres content and controls its width |
border-bottom | Adds the lower border |
mb-3 | Adds spacing below the navigation |
7. Replace wwwroot/css/site.css
html {
font-size: 14px;
position: relative;
min-height: 100%;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
body {
margin-bottom: 60px;
}
.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;
}
.error-code {
color: #b02a37;
}
Bootstrap now provides most of the design, so the project-specific stylesheet is much smaller.
8. Update Pages/Index.cshtml
@page
@model IndexModel
@{
ViewData["Title"] = "Home";
}
<div class="text-center">
<h1 class="display-4">
Welcome to basicdotnet
</h1>
<p>
A simple ASP.NET Core web app built
with Razor Pages, running on Linux.
</p>
<p>
It's <span id="server-time">loading...</span>
on the server.
</p>
</div>
@section Scripts {
<script>
const serverTimeElement =
document.getElementById("server-time");
fetch("/?handler=Time")
.then(response => {
if (!response.ok) {
throw new Error(
"Unable to retrieve server time."
);
}
return response.text();
})
.then(serverTime => {
serverTimeElement.textContent = serverTime;
})
.catch(() => {
serverTimeElement.textContent = "unknown";
});
</script>
}
9. Understand the optional Scripts section
The page defines:
@section Scripts {
<script>
// Page-specific JavaScript
</script>
}
The layout renders it near the end of <body>:
@await RenderSectionAsync("Scripts", required: false)
required: false means that pages without JavaScript do not need to define the section.
10. Update Pages/Privacy.cshtml
@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>
basicdotnet is a demo app. It stores no
personal data and sets no cookies.
</p>
<p>
Any information you enter while exploring
is kept only in memory for the current session.
</p>
<p>
For questions, contact the site administrator.
</p>
11. Update the Home link on the 404 page
In Pages/NotFound.cshtml, replace the hard-coded link with:
<a asp-page="/Index">Return to the Home page</a>
12. Keep the application configuration unchanged
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.MapRazorPages();
app.MapFallbackToPage("/NotFound");
app.Run();
13. Build and run
dotnet build
dotnet run
Open the address printed in the terminal.
14. Test the responsive menu
- Open the Home page.
- Reduce the browser width.
- Confirm that the Home and Privacy links collapse.
- Click the menu button.
- Confirm that Bootstrap expands the navigation links.
If the button appears but does not work, verify that the Bootstrap bundle script is present near the end of _Layout.cshtml.
15. Inspect the generated navigation link
Although the source uses:
<a asp-page="/Privacy">Privacy</a>
The browser receives ordinary HTML similar to:
<a href="/Privacy">Privacy</a>
Tag Helpers run on the server; browsers never receive the asp-page attribute.
What has been achieved?
- The navigation resembles the default Razor Pages interface.
- The navigation collapses on narrow screens.
- Page links use Razor Tag Helpers.
- The stylesheet uses cache-busting version information.
- Page-specific JavaScript is placed in an optional layout section.
- A shared footer has been added.
The appearance is now close to the default ASP.NET Core Razor Pages template. Bootstrap is still loaded from an external CDN rather than from the project itself.
Next: PART 10 installs the client libraries locally, adds the remaining default template files, and completes the transition to the standard ASP.NET Core 8 Razor Pages project.