diff --git a/.ai-team/agents/cyclops/history.md b/.ai-team/agents/cyclops/history.md
index 7363ca5d..384e6524 100644
--- a/.ai-team/agents/cyclops/history.md
+++ b/.ai-team/agents/cyclops/history.md
@@ -16,6 +16,12 @@
- **Pre-existing test infrastructure issue:** The test project on `dev` has a broken `AddXUnit` reference in `BlazorWebFormsTestContext.cs` — this is not caused by component changes.
- **FileUpload must use InputFile internally:** Raw `` with `@onchange` receives `ChangeEventArgs` (no file data). Must use Blazor's `InputFile` component which provides `InputFileChangeEventArgs` with `IBrowserFile` objects. The `@using Microsoft.AspNetCore.Components.Forms` directive is needed in the `.razor` file since `_Imports.razor` only imports `Microsoft.AspNetCore.Components.Web`.
- **Path security in file save operations:** `Path.Combine` silently drops earlier arguments if a later argument is rooted (e.g., `Path.Combine("uploads", "/etc/passwd")` returns `/etc/passwd`). Always use `Path.GetFileName()` to sanitize filenames and validate resolved paths with `Path.GetFullPath()` + `StartsWith()` check.
+- **PageService event handler catch pattern:** In `Page.razor.cs`, async event handlers that call `InvokeAsync(StateHasChanged)` should catch `ObjectDisposedException` (not generic `Exception`) — the component may be disposed during navigation while an event is still in flight. This is the standard Blazor pattern for disposed-component safety.
+- **Test dead code:** Code scanning flags unused variable assignments in test files. Use `_ = expr` discard for side-effect-only calls, and remove `var` assignments where the result is never asserted.
+- **ImageMap base class fix:** ImageMap inherits `BaseStyledComponent` (not `BaseWebFormsComponent`), matching the Web Forms `ImageMap → Image → WebControl` hierarchy. This gives it CssClass, Style, Font, BackColor, etc. The `@inherits` directive in `.razor` must match the code-behind.
+- **Instance-based IDs for generated HTML IDs:** Never use `static` counters for internal element IDs (like map names) — they leak across test runs and create non-deterministic output. Use `Guid.NewGuid()` as a field initializer instead.
+- **ImageAlign rendering:** `.ToString().ToLower()` on `ImageAlign` enum values produces the correct Web Forms output (`absbottom`, `absmiddle`, `texttop`). No custom mapping needed.
+- **Enabled propagation pattern:** When `Enabled=false` on a styled component, interactive child elements (like `` in ImageMap) should render as inactive (nohref, no onclick). Check `Enabled` from `BaseWebFormsComponent` — it defaults to `true`.
📌 Team update (2026-02-10): FileUpload needs InputFile integration — @onchange won't populate file data. Ship-blocking bug. — decided by Forge
📌 Team update (2026-02-10): ImageMap base class must be BaseStyledComponent, not BaseWebFormsComponent — decided by Forge
diff --git a/README.md b/README.md
index 5173e197..f66eb4a9 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,7 @@ There are a handful of features that augment the ASP.NET developmen
- [DataBinder](docs/UtilityFeatures/Databinder.md)
- [ID Rendering](docs/UtilityFeatures/IDRendering.md) - Render HTML IDs for JavaScript integration
- [JavaScript Setup](docs/UtilityFeatures/JavaScriptSetup.md) - Options for auto-loading required JavaScript
+ - [PageService](docs/UtilityFeatures/PageService.md) - Programmatic page title setting (Page.Title equivalent)
- [ViewState](docs/UtilityFeatures/ViewState.md)
### Custom Control Migration Support
diff --git a/docs/UtilityFeatures/PageService.md b/docs/UtilityFeatures/PageService.md
new file mode 100644
index 00000000..0701f3b1
--- /dev/null
+++ b/docs/UtilityFeatures/PageService.md
@@ -0,0 +1,305 @@
+# PageService
+
+## Background
+
+In ASP.NET Web Forms, the `Page` object provided a central place for page-level properties and functionality. Key properties included `Page.Title`, `Page.MetaDescription`, and `Page.MetaKeywords`, which allowed developers to programmatically set page metadata that appears in the browser and search engine results.
+
+```csharp
+// Web Forms code-behind
+protected void Page_Load(object sender, EventArgs e)
+{
+ Page.Title = "My Dynamic Page Title";
+ Page.MetaDescription = "Description for search engines";
+ Page.MetaKeywords = "keyword1, keyword2, keyword3";
+}
+
+protected void UpdateButton_Click(object sender, EventArgs e)
+{
+ Page.Title = "Title Updated - " + DateTime.Now.ToString();
+ Page.MetaDescription = GetDescriptionFromDatabase();
+}
+```
+
+This pattern was essential for:
+- Setting page titles dynamically based on data or user actions
+- Implementing SEO-friendly titles and descriptions for content pages
+- Providing context-specific metadata in master page scenarios
+- Improving search engine visibility and social media sharing
+
+## Web Forms Usage
+
+In Web Forms, the `Page` object was automatically available in all code-behind files:
+
+```aspx
+<%@ Page Language="C#" Title="Static Title"
+ MetaDescription="Page description"
+ MetaKeywords="keyword1, keyword2" %>
+```
+
+```csharp
+// Code-behind (.aspx.cs)
+public partial class MyPage : System.Web.UI.Page
+{
+ protected void Page_Load(object sender, EventArgs e)
+ {
+ // Dynamically set page metadata
+ Page.Title = GetTitleFromDatabase();
+ Page.MetaDescription = GetDescriptionFromDatabase();
+ Page.MetaKeywords = GetKeywordsFromDatabase();
+ }
+
+ private string GetTitleFromDatabase()
+ {
+ // Fetch from database
+ return "Dynamic Title from DB";
+ }
+
+ private string GetDescriptionFromDatabase()
+ {
+ return "This is a dynamic description for SEO";
+ }
+
+ private string GetKeywordsFromDatabase()
+ {
+ return "blazor, webforms, migration, seo";
+ }
+}
+```
+
+These properties would automatically update the HTML `
` and `` elements in the rendered page.
+
+## Blazor Implementation
+
+BlazorWebFormsComponents provides `IPageService` and `PageService` to replicate this functionality in Blazor. The service is registered as a scoped service (one instance per request/render cycle) and can be injected into any component or page.
+
+### Key Components
+
+1. **IPageService Interface** - Defines the contract for page-level services
+2. **PageService Class** - Default implementation providing `Title`, `MetaDescription`, and `MetaKeywords` properties
+3. **Page Component** - Blazor component that renders the dynamic `` and `` tags
+
+### Registration
+
+The service is automatically registered when you call `AddBlazorWebFormsComponents()`:
+
+```csharp
+// Program.cs
+builder.Services.AddBlazorWebFormsComponents();
+```
+
+This registers `IPageService` as a scoped service that can be injected into components.
+
+### Usage in Blazor
+
+**Step 1: Add the Page component to your page or layout**
+
+```razor
+@inject IPageService Page
+
+ @* This renders the dynamic and meta tags *@
+```
+
+**Step 2: Set page properties programmatically**
+
+```razor
+@code {
+ protected override void OnInitialized()
+ {
+ Page.Title = "My Dynamic Page Title";
+ Page.MetaDescription = "Description for search engines";
+ Page.MetaKeywords = "blazor, webforms, migration";
+ }
+
+ private void UpdateMetadata()
+ {
+ Page.Title = "Updated Title - " + DateTime.Now.ToString();
+ Page.MetaDescription = "Updated description based on user action";
+ }
+}
+```
+
+### Complete Example
+
+```razor
+@page "/MyPage"
+@inject IPageService Page
+
+
+
+
+
+@code {
+ [Parameter]
+ public int Id { get; set; }
+
+ protected override async Task OnInitializedAsync()
+ {
+ var customerName = await GetCustomerName(Id);
+ Page.Title = $"Customer Details - {customerName}";
+ Page.MetaDescription = $"View detailed information for {customerName}";
+ Page.MetaKeywords = "customer, details, crm";
+ }
+}
+```
+
+## Features
+
+### Title Property
+
+- **Get/Set**: Read and write the page title dynamically
+- **Event-Driven**: `TitleChanged` event fires when title is updated
+- **Reactive**: The `Page` component automatically updates the browser title when the property changes
+
+### MetaDescription Property
+
+- **Get/Set**: Read and write the meta description dynamically
+- **SEO-Friendly**: Appears in search engine results (recommended 150-160 characters)
+- **Event-Driven**: `MetaDescriptionChanged` event fires when description is updated
+- **Reactive**: The `Page` component automatically updates the meta tag when the property changes
+
+### MetaKeywords Property
+
+- **Get/Set**: Read and write the meta keywords dynamically
+- **SEO Support**: Helps categorize page content for search engines
+- **Event-Driven**: `MetaKeywordsChanged` event fires when keywords are updated
+- **Reactive**: The `Page` component automatically updates the meta tag when the property changes
+
+### Future Extensibility
+
+The `IPageService` interface can be extended in future versions to support additional `Page` object features:
+
+- Open Graph meta tags for social media
+- Page-level client script registration
+- Page-level CSS registration
+- Custom meta tags
+
+## Key Differences from Web Forms
+
+| Web Forms | Blazor with PageService | Notes |
+|-----------|------------------------|-------|
+| `Page.Title` property | `Page.Title` property | Same API! Just inject `IPageService` as `Page` |
+| `Page.MetaDescription` property | `Page.MetaDescription` property | Available in Web Forms .NET 4.0+ |
+| `Page.MetaKeywords` property | `Page.MetaKeywords` property | Available in Web Forms .NET 4.0+ |
+| Available automatically | Must inject `IPageService` as `Page` | Standard Blazor DI pattern |
+| Synchronous | Synchronous | No change needed |
+| Scoped to request | Scoped to render cycle | Similar lifecycle |
+
+## Moving On
+
+While `PageService` provides familiar Web Forms compatibility, consider these Blazor-native approaches:
+
+### For Static Metadata
+
+Use the built-in components directly:
+
+```razor
+@page "/about"
+
+About Us - My Company
+
+
+
+
+
+
About Us
+```
+
+### For Dynamic Metadata
+
+The `PageService` approach is appropriate when:
+- Metadata depends on data loaded asynchronously
+- Metadata changes based on user actions
+- Metadata is set in response to events
+- You want Web Forms-style programmatic control
+
+For simpler scenarios, you can use built-in components with bound variables:
+
+```razor
+@currentTitle
+
+
+
+
+@code {
+ private string currentTitle = "Default Title";
+ private string currentDescription = "Default description";
+
+ private void UpdateTitle(string newTitle)
+ {
+ currentTitle = newTitle;
+ }
+}
+```
+
+## Best Practices
+
+1. **Set Title Early**: Set the title in `OnInitializedAsync` or `OnParametersSet` to ensure it's available before first render
+2. **SEO Considerations**: Provide meaningful, descriptive titles for better search engine optimization
+3. **User Context**: Include relevant context in the title (e.g., customer name, product name)
+4. **Length**: Keep titles under 60 characters for optimal display in browser tabs and search results
+5. **Consistent Pattern**: Use a consistent title format across your application (e.g., "Page Name - Site Name")
+
+## See Also
+
+- [Live Sample](https://blazorwebformscomponents.azurewebsites.net/UtilityFeatures/PageService)
+- [Microsoft Docs: Page.Title Property](https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.page.title?view=netframework-4.8)
+- [Blazor PageTitle Component](https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing#page-title)
diff --git a/mkdocs.yml b/mkdocs.yml
index bbb1d7b8..baa8e5d3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -113,6 +113,7 @@ nav:
- Databinder: UtilityFeatures/Databinder.md
- ID Rendering: UtilityFeatures/IDRendering.md
- JavaScript Setup: UtilityFeatures/JavaScriptSetup.md
+ - PageService: UtilityFeatures/PageService.md
- ViewState: UtilityFeatures/ViewState.md
- Migration:
- Getting started: Migration/readme.md
diff --git a/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor b/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor
index 27a594ad..df1ee667 100644
--- a/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor
+++ b/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor
@@ -129,6 +129,9 @@
+
+
+
diff --git a/samples/AfterBlazorServerSide/Components/Pages/UtilityFeatures/PageService/Index.razor b/samples/AfterBlazorServerSide/Components/Pages/UtilityFeatures/PageService/Index.razor
new file mode 100644
index 00000000..3b7bd710
--- /dev/null
+++ b/samples/AfterBlazorServerSide/Components/Pages/UtilityFeatures/PageService/Index.razor
@@ -0,0 +1,163 @@
+@page "/UtilityFeatures/PageService"
+@inject IPageService Page
+
+
+
+
PageService - Web Forms Page Object Compatibility
+
+
The PageService provides page-level services that mimic ASP.NET Web Forms Page object functionality, enabling programmatic access to page properties like Title, MetaDescription, and MetaKeywords.
+
+
+
Current Page Properties
+
Title: @Page.Title
+
Meta Description: @(string.IsNullOrEmpty(Page.MetaDescription) ? "(not set)" : Page.MetaDescription)
+
Meta Keywords: @(string.IsNullOrEmpty(Page.MetaKeywords) ? "(not set)" : Page.MetaKeywords)