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?

BeforeAfter
Custom navigation classesBootstrap navbar classes
href="/privacy"asp-page="/Privacy"
Custom mobile wrappingBootstrap collapsible navigation
Script inside page contentOptional Razor Scripts section
No footerShared 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">
            &copy; 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:

ClassPurpose
navbarCreates the navigation component
navbar-expand-smExpands navigation on small and larger screens
navbar-togglerCreates the mobile menu button
collapse navbar-collapseControls the collapsible link area
containerCentres content and controls its width
border-bottomAdds the lower border
mb-3Adds 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

  1. Open the Home page.
  2. Reduce the browser width.
  3. Confirm that the Home and Privacy links collapse.
  4. Click the menu button.
  5. 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.