Skip to content

Commit feca282

Browse files
authored
docs: Added Finalized Routing Specifications, and multiple drafts for related topics (#137)
1 parent 16ece1e commit feca282

15 files changed

+4580
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
authors:
3+
- Martin Stühmer (https://github.com/samtrion)
4+
5+
applyTo:
6+
- "src/ForgingBlazor/**/*.cs"
7+
- "src/ForgingBlazor/**/*.razor"
8+
- "src/ForgingBlazor.Extensibility/**/*.cs"
9+
- "specs/routing.md"
10+
11+
created: 2026-01-25
12+
13+
lastModified: 2026-01-25
14+
15+
state: accepted
16+
17+
instructions: |
18+
ForgingBlazor uses content-based routing with compile-time segment discovery.
19+
MUST define all routes via ConfigureRouting<TApp, THome>() - no runtime discovery.
20+
MUST use ForgingRouter/ForgingRouteView instead of Blazor Router/RouteView.
21+
Template components MUST NOT contain @page or @layout directives.
22+
MUST use Static Assets caching in wwwroot with ExcludeFromCache() for dynamic content.
23+
MUST use TimeProvider via constructor injection for time-based operations.
24+
MUST use separate IAssetProvider for images/CSS (not in content folders).
25+
Route constraints: Blazor standard + alpha (slugs) + lang (culture with inheritance).
26+
Segment nesting: Soft limit 5 levels (warning), hard limit 8 levels (error).
27+
---
28+
29+
# Content-Based Routing Architecture
30+
31+
This ADR documents the architectural decisions for ForgingBlazor's content-based routing system.
32+
33+
## Context
34+
35+
ForgingBlazor requires a routing mechanism that serves content from multiple storage providers (Physical FileSystem, Azure Blob Storage) while maintaining compatibility with Blazor's component model.
36+
37+
Key requirements:
38+
39+
- Support content-based routing from configurable storage providers
40+
- Enable content synchronization between local and cloud storage
41+
- Maintain Blazor compatibility for non-content routes
42+
- Support multi-language content with culture fallback
43+
- Provide efficient caching for static content
44+
45+
## Decision
46+
47+
### 1. Compile-Time Segment Discovery
48+
49+
All routes MUST be explicitly defined via `ConfigureRouting<TApp, THome>()`. Runtime discovery from storage structure is not supported.
50+
51+
**Rationale:**
52+
53+
- Type safety: All routes validated at compile-time
54+
- Performance: No storage scanning at startup or runtime
55+
- Security: No unintended routes exposed
56+
- Predictability: Route structure explicit in code
57+
58+
### 2. ForgingRouter and ForgingRouteView
59+
60+
Custom router components replace Blazor's `Router` and `RouteView`:
61+
62+
- `ForgingRouter`: Combines Blazor `@page` routes with ForgingBlazor content routes
63+
- `ForgingRouteView`: Provides content via `CascadingParameter<ResolvedContent<T>>`
64+
65+
ForgingBlazor routes take precedence over `@page` routes for same paths.
66+
67+
### 3. Template Component Restrictions
68+
69+
Components used as `TPageTemplate` or `TSegmentTemplate` MUST NOT contain:
70+
71+
- `@page` directive (routing managed by ForgingBlazor)
72+
- `@layout` directive (layout inherited through routing hierarchy)
73+
74+
**Enforcement:** Roslyn Analyzer (warning) + Runtime validation (error).
75+
76+
### 4. Route Constraints
77+
78+
Blazor/ASP.NET Core standard constraints plus ForgingBlazor extensions:
79+
80+
- `alpha`: URL-friendly slugs (`a-zA-Z0-9-`)
81+
- `lang`: Culture codes with automatic inheritance to nested elements
82+
83+
### 5. Static Asset Caching
84+
85+
Content is rendered to static HTML files in `wwwroot`. Web server serves files directly, bypassing application code.
86+
87+
**Cache Invalidation Triggers:**
88+
89+
- Content change: Remove specific static asset
90+
- Application restart: Clear all static assets
91+
- `ExcludeFromCache()`: Opt-out for dynamic content
92+
93+
### 6. Segment Nesting Limits
94+
95+
- Soft Limit: 5 levels (warning)
96+
- Hard Limit: 8 levels (error)
97+
98+
### 7. Asset Management
99+
100+
Assets managed via separate `IAssetProvider` interface with dedicated storage directory. Assets are NOT stored within content folders.
101+
102+
### 8. TimeProvider Usage
103+
104+
`TimeProvider` provided via constructor injection for all time-based operations (publish dates, expiry dates).
105+
106+
### 9. Storage Provider Architecture
107+
108+
- Core library (`ForgingBlazor`): Physical FileSystem provider + synchronization
109+
- Separate library (`ForgingBlazor.Providers.AzureBlob`): Azure Blob Storage provider
110+
- Synchronization uses fast-exit pattern (no-op for single provider)
111+
112+
### 10. Culture Fallback Chain
113+
114+
For requested culture `de-DE`:
115+
116+
```
117+
de-DE → de → en-US → en → (no suffix)
118+
```
119+
120+
## Consequences
121+
122+
### Benefits
123+
124+
| Benefit | Description |
125+
| ------------------------ | ----------------------------------------------------- |
126+
| **Type Safety** | Compile-time route validation |
127+
| **Performance** | Static file serving, no runtime content resolution |
128+
| **Blazor Compatibility** | Standard `@page` routes work alongside content routes |
129+
| **Flexibility** | Multiple storage providers, culture support |
130+
| **Testability** | TimeProvider injection enables time-based testing |
131+
132+
### Trade-offs
133+
134+
| Trade-off | Mitigation |
135+
| ------------------------------- | ------------------------------------------------------- |
136+
| **No Dynamic Segments** | Use parameterized pages (`{slug}`) for flexible content |
137+
| **Deployment for New Segments** | Redeploy application with updated segment definitions |
138+
| **Cache Staleness** | Application restart or explicit invalidation |
139+
140+
### Risks
141+
142+
| Risk | Mitigation |
143+
| ------------------ | -------------------------------------------------- |
144+
| **Disk Space** | Monitor `wwwroot` folder size in production |
145+
| **Layout Changes** | Application restart invalidates all cached content |
146+
147+
## Alternatives Considered
148+
149+
### Runtime Segment Discovery
150+
151+
**Rejected:** Would compromise type safety, performance, and security.
152+
153+
### In-Memory Caching
154+
155+
**Rejected:** Static assets provide better performance and CDN compatibility.
156+
157+
### Integrated Asset Storage
158+
159+
**Rejected:** Separate storage enables CDN integration and independent asset management.
160+
161+
### TimeProvider via Static Property
162+
163+
**Rejected:** Constructor injection provides better testability and consistency.
164+
165+
## Related Decisions
166+
167+
- [DateTimeOffset and TimeProvider Usage](./2026-01-21-datetimeoffset-and-timeprovider-usage.md) - TimeProvider injection pattern
168+
- [Folder Structure and Naming Conventions](./2025-07-10-folder-structure-and-naming-conventions.md) - Project organization
169+
- [.NET 10 / C# 13 Adoption](./2025-07-11-dotnet-10-csharp-13-adoption.md) - Framework version
170+
171+
## Related Specifications
172+
173+
- [Routing Specification](../specs/routing.md) - Complete technical specification
174+
- [Layout Change Detection](../specs/layout-change-detection.md) - Future consideration
175+
- [Route Conflict Resolution](../specs/route-conflict-resolution.md) - Future consideration
176+
- [Content Authorization](../specs/content-authorization.md) - Future consideration
177+
- [Content SEO](../specs/content-seo.md) - Future consideration

specs/content-aliases.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Content Aliases
2+
3+
> **Status:** Draft
4+
> **Version:** 0.1.0
5+
> **Created:** 2026-01-25
6+
> **Last Modified:** 2026-01-25
7+
> **Author:** [Martin Stühmer](https://github.com/samtrion)
8+
> **Parent:** [Routing Specification](./routing.md)
9+
10+
---
11+
12+
## 1. Overview
13+
14+
This specification defines content aliasing for ForgingBlazor.
15+
16+
Content aliases enable alternative URL paths to serve the same content, supporting URL migrations and canonical URL management.
17+
18+
---
19+
20+
## 2. Problem Statement
21+
22+
Content often needs to be accessible via multiple URLs:
23+
24+
- Legacy URL redirects after site restructuring
25+
- Shortened URLs for sharing
26+
- Alternative slugs (e.g., `/posts/2026/01/hello` and `/posts/hello-world`)
27+
28+
### 2.1 Requirements
29+
30+
| Requirement | Description |
31+
| ------------------------- | ------------------------------------------------ |
32+
| **Multiple Aliases** | Single content accessible via multiple URL paths |
33+
| **Canonical Declaration** | Define the primary/canonical URL for SEO |
34+
| **Redirect Support** | HTTP 301 redirects from alias to canonical URL |
35+
36+
---
37+
38+
## 3. Goals
39+
40+
| Goal | Description |
41+
| --------------------- | ---------------------------------------------- |
42+
| **SEO Preservation** | Maintain search rankings during URL migrations |
43+
| **Flexibility** | Support various aliasing patterns |
44+
| **Canonical Control** | Ensure proper canonical URL declaration |
45+
46+
---
47+
48+
## 4. Proposed Interface
49+
50+
```csharp
51+
public interface IContentAliasProvider
52+
{
53+
ValueTask<ContentAlias?> ResolveAliasAsync(
54+
string aliasPath,
55+
CancellationToken cancellationToken = default);
56+
57+
ValueTask<IReadOnlyList<string>> GetAliasesAsync(
58+
string contentPath,
59+
CancellationToken cancellationToken = default);
60+
61+
ValueTask<string?> GetCanonicalPathAsync(
62+
string contentPath,
63+
CancellationToken cancellationToken = default);
64+
}
65+
66+
public record ContentAlias
67+
{
68+
public required string AliasPath { get; init; }
69+
public required string TargetPath { get; init; }
70+
}
71+
```
72+
73+
---
74+
75+
## 5. Frontmatter Schema
76+
77+
```yaml
78+
---
79+
title: "Hello World"
80+
aliases:
81+
- /old-blog/hello-world
82+
- /posts/2026/01/25/hello
83+
- /hw
84+
---
85+
```
86+
87+
All aliases redirect (HTTP 301) to the canonical URL (the actual content path).
88+
89+
---
90+
91+
## 6. Configuration
92+
93+
```csharp
94+
services.AddForgingBlazor(options =>
95+
{
96+
options.Aliases.Enabled = true;
97+
options.Aliases.ValidateOnStartup = true;
98+
});
99+
```
100+
101+
---
102+
103+
## 7. Canonical URL Handling
104+
105+
When aliases exist, the canonical URL is automatically set in the HTML head:
106+
107+
```html
108+
<!-- For request to /old-blog/hello-world (alias) → redirects to canonical -->
109+
<!-- At /posts/hello-world (canonical) -->
110+
<link rel="canonical" href="https://example.com/posts/hello-world" />
111+
```
112+
113+
### 7.1 Frontmatter Override
114+
115+
For content originally published elsewhere:
116+
117+
```yaml
118+
---
119+
title: "Hello World"
120+
canonical: https://external-site.com/original-post
121+
---
122+
```
123+
124+
---
125+
126+
## 8. Alias Validation
127+
128+
Aliases are validated at startup:
129+
130+
| Validation | Behavior |
131+
| ----------------------- | ------------------------------------------------ |
132+
| **Target Exists** | Warning if alias points to non-existent content |
133+
| **Circular References** | Error if A → B → A |
134+
| **Duplicate Aliases** | Error if same alias defined for multiple targets |
135+
136+
---
137+
138+
## 9. Use Cases
139+
140+
### 9.1 URL Migration
141+
142+
```yaml
143+
# Old URL structure preserved after migration
144+
aliases:
145+
- /2026/01/25/my-post.html
146+
- /archive/my-post
147+
```
148+
149+
### 9.2 Vanity URLs
150+
151+
```yaml
152+
# Short URLs for marketing/sharing
153+
aliases:
154+
- /go/product-launch
155+
- /promo
156+
```
157+
158+
---
159+
160+
## 10. Open Questions
161+
162+
- Should aliases support query string preservation?
163+
- How to handle alias conflicts with configured routes?
164+
165+
---
166+
167+
## Revision History
168+
169+
| Version | Date | Author | Changes |
170+
| ------- | ---------- | --------------------------------------------- | ------------- |
171+
| 0.1.0 | 2026-01-25 | [Martin Stühmer](https://github.com/samtrion) | Initial draft |

0 commit comments

Comments
 (0)