aRustyDev / aRustyDev/mdbook-htmx
docs(adr): ADR-0009: Audience Scopes for Filtered Documentation Views
- Dominant language
- Rust
- Stars
- 0
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
# ADR-0009: Audience Scopes for Filtered Documentation Views
## Status
Accepted
## Context
Documentation often serves multiple audiences with different needs:
- **Developers** need API references, code examples
- **Managers** need architecture overviews, decision rationale
- **SREs** need operational runbooks, monitoring guides
- **Beginners** need tutorials, getting started guides
Users authorized to see multiple audiences' content want to **focus** on one audience at a time, filtering navigation and search to that subset.
## Decision Drivers
1. **Focus** - Users shouldn't wade through irrelevant content
2. **Security** - Users shouldn't see content they're not authorized for
3. **Simplicity** - Minimal configuration burden on content authors
4. **Performance** - Fast filtering without server-side computation
## Key Decisions
### 1. URL Structure: Query Parameter
**Decision**: Use query parameter `?scope=developers` instead of path prefix.
```
/docs/chapter-1?scope=developers ✅ Chosen
/docs/developers/chapter-1 ❌ Rejected
```
**Rationale**:
- Content URL is stable regardless of scope
- Better caching (same page, different lens)
- No path rewriting needed for internal links
- Scope is a "view preference" not a location
### 2. No Scope Inheritance
**Decision**: Each page explicitly declares its scopes. No inheritance from parent chapters.
```yaml
# Every page must declare its scopes
---
scopes: [developers, sre]
---
```
**Rationale**:
- Explicit is better than implicit
- Easy to audit (grep for scopes)
- No surprises when restructuring chapters
- Avoids complex inheritance resolution
### 3. Separate Search Indexes Per Scope
**Decision**: Generate scope-specific search indexes at build time.
```
book/htmx/
├── search-index.json # Full index
├── search-index.developers.json # Developer scope only
├── search-index.managers.json # Manager scope only
```
**Rationale**:
- Prevents information leakage (titles, snippets)
- No runtime filtering overhead
- Unauthorized content never sent to client
### 4. Runtime AuthZ as Defense in Depth
**Decision**: Apply authorization filtering at runtime even with scope-specific indexes.
**Rationale**:
- Build-time scope filtering may drift from auth rules
- Defense in depth catches edge cases
- Minimal performance impact
## Implementation
### Frontmatter Schema
```yaml
---
title: API Reference
scopes:
- developers
- sre
auth:
access: roles
roles: [developer, sre]
---
```
Note: `scopes` and `auth` are independent:
- `scopes` controls navigation filtering (user's choice)
- `auth` controls access permissions (admin's policy)
### Configuration
```toml
[output.htmx.scopes]
enabled = true
available = ["all", "developers", "managers", "sre"]
default = "all"
[output.htmx.search]
scope-indexes = true # Generate per-scope indexes
authz-filter = true # Runtime auth check
global-scopes = ["all"] # Always in main index
```
### Build Output
```
book/htmx/
├── manifest.json # Full manifest
├── manifest.developers.json # Navigation for developer scope
├── manifest.managers.json # Navigation for manager scope
├── search-index.json # Full search index
├── search-index.developers.json
└── search-index.managers.json
```
### Server Implementation
```typescript
app.get('/docs/*', async (c) => {
const scope = c.req.query('scope') || 'all';
const user = c.get('user');
// Anonymous users forced to 'all' scope
const effectiveScope = user ? scope : 'all';
// Load scope-specific manifest for navigation
const manifest = manifests[effectiveScope] || manifests.all;
// Load scope-specific search index
const searchIndex = searchIndexes[effectiveScope] || searchIndexes.all;
return c.html(await render('layout.html', {
page,
navigation: manifest.navigation,
currentScope: effectiveScope,
availableScopes: user ? manifest.scopes.available : ['all']
}));
});
```
### Scope Switcher Partial
```html
View:
{% for scope in available_scopes %}
{{ scope | title }}
{% endfor %}
```
## Scope vs Authorization
| Aspect | Scope | Authorization |
|--------|-------|---------------|
| **Question** | What view do I want? | Can I access this page? |
| **Who decides** | User (their preference) | Admin (policy) |
| **Enforcement** | Navigation + search filtering | Access denied / redirect |
| **Failure mode** | Page hidden from nav/search | 403 or redirect to login |
A page can be:
- In scope `developers` but require role `senior-developer`
- Visible in navigation but return 403 on access
- In multiple scopes with the same authorization
## Consequences
### Positive
- Users can focus on relevant content
- Search results are scoped appropriately
- No information leakage via search
- Clean URL structure
- Simple explicit configuration
### Negative
- More build output (one manifest/index per scope)
- Authors must tag every page with scopes
- No automatic scope inference
### Mitigation
- Default scope `all` catches untagged pages
- Linting can warn about missing scope declarations
- Build-time validation ensures consistency
## Alternatives Considered
### Path-Based Scopes (`/docs/developers/page`)
**Rejected** because:
- Breaks internal links (need rewriting)
- Complicates caching
- Scope is a lens, not a location
### Scope Inheritance from Parent Chapters
**Rejected** because:
- Implicit behavior causes surprises
- Hard to audit
- Restructuring chapters changes scope unexpectedly
### Single Index with Runtime Filtering
**Rejected** because:
- Leaks page titles and snippets
- Runtime overhead on every search
- Defense-only approach (no build-time separation)
## References
- [Audience-based content in documentation](https://www.writethedocs.org/guide/writing/audience/)
- [Faceted navigation patterns](https://www.nngroup.com/articles/faceted-search/)
Contributor guide
Assessment
This issue has not been assessed yet.