aRustyDev / aRustyDev/mdbook-htmx

docs(adr): ADR-0016: Implementation Phasing Strategy

Open
#24 0 comments 0 reactions 1 assignee Claimed by @aRustyDev View on GitHub
documentation
Dominant language
Rust
Stars
0
Forks
1
PR merge metrics
No merged PRs in 30d

Description

# ADR-0016: Implementation Phasing Strategy

## Status

Accepted

## Context

mdbook-htmx is a complex project with many features: template rendering, HTMX integration, authorization, search indexing, scope filtering, and asset handling. Implementing everything at once would be risky and make debugging difficult.

This ADR defines a phased implementation strategy that delivers value incrementally while managing complexity.

## Decision Drivers

1. **Early Value** - Ship usable output as soon as possible
2. **Risk Mitigation** - Isolate complex features for easier debugging
3. **Testing Surface** - Each phase should be independently testable
4. **Dependency Order** - Features must be built in the right order
5. **User Feedback** - Get feedback before building optional features

## Decision

**Implement in 5 phases, each with clear deliverables, tests, and release milestones.**

### Phase Overview

| Phase | Name | Focus | Release |
|-------|------|-------|---------|
| 1 | Core Backend | Basic rendering | v0.1.0 |
| 2 | HTMX Integration | Navigation patterns | v0.2.0 |
| 3 | Authorization | Auth metadata + manifest | v0.3.0 |
| 4 | Search | Index generation | v0.4.0 |
| 5 | Polish | Theming, scopes, assets | v1.0.0 |

### Deployment Methods

Three deployment methods evolve alongside the implementation:

| Method | Location | Purpose |
|--------|----------|---------|
| Docker Compose | `mdbook-htmx/docker/` | Local dev, CI testing |
| Helm Chart | `aRustyDev/helm-charts/charts/mdbook-htmx` | Kubernetes |
| Cloudflare | `mdbook-htmx/workers/` + `wrangler.jsonc` | Production edge |

Each phase adds deployment features:

| Phase | Docker Compose | Helm Chart | Cloudflare |
|-------|----------------|------------|------------|
| v0.1.0 | NGINX static | Static serve | Pages static |
| v0.2.0 | +fragment routing | +Ingress rules | +fragment routing |
| v0.3.0 | +OAuth2 proxy | +auth sidecar | +Workers D1 auth |
| v0.4.0 | +Meilisearch | +Meilisearch | +KV search index |
| v1.0.0 | +cache, metrics | +HPA, PDB | +R2 assets, analytics |

---

## Phase 1: Core Backend (v0.1.0)

### Goal

Produce valid HTML output from MDBook's RenderContext.

### Deliverables

| Component | Description |
|-----------|-------------|
| Entry point | Read RenderContext from stdin |
| Configuration | Parse `[output.htmx]` from book.toml |
| Template engine | Initialize Tera with default templates |
| Chapter rendering | Render each chapter to HTML |
| Output structure | Create pages/ and fragments/ directories |
| Manifest | Generate basic manifest.json |

### Implementation Order

```
1. Cargo setup + dependencies
2. main.rs entry point
3. Config deserialization
4. Template loading
5. Chapter iteration
6. HTML output
7. Manifest generation
```

### Exit Criteria

- [ ] `mdbook build` produces HTML in `book/htmx/`
- [ ] Pages render correctly without HTMX attributes
- [ ] Manifest contains all pages with paths and titles
- [ ] Default templates bundled in binary
- [ ] Custom theme override works

### Deployment (Phase 1)

| Method | Deliverables |
|--------|--------------|
| Docker Compose | `compose.yml`, `nginx.conf`, `Dockerfile.build` |
| Helm Chart | Chart skeleton with deployment, service, configmap |
| Cloudflare | `wrangler.jsonc`, basic `workers/index.ts` |
| CI | `test-deployments.yml`, `deploy-cloudflare.yml` |

### Test Cases

```rust
#[test]
fn test_basic_render() {
let ctx = test_render_context();
let output = render(&ctx).unwrap();
assert!(output.join("pages/index.html").exists());
}

#[test]
fn test_manifest_generation() {
let ctx = test_render_context();
render(&ctx).unwrap();
let manifest: Manifest = serde_json::from_reader(
File::open(ctx.destination.join("manifest.json")).unwrap()
).unwrap();
assert_eq!(manifest.pages.len(), ctx.book.iter().count());
}
```

---

## Phase 2: HTMX Integration (v0.2.0)

### Goal

Enable SPA-like navigation with HTMX attributes.

### Deliverables

| Component | Description |
|-----------|-------------|
| hx-boost | Add to body element |
| hx-target | Default content target |
| hx-swap | Default swap strategy |
| hx-push-url | URL updates |
| Fragments | Content-only partials |
| OOB swaps | Sidebar + breadcrumb updates |

### Implementation Order

```
1. Update layout template with HTMX body attributes
2. Fragment template (content without layout)
3. OOB partial templates
4. Sidebar active state
5. Breadcrumb generation
6. Navigation links with hx-* attributes
```

### Exit Criteria

- [ ] Clicking links loads content without full page reload
- [ ] Sidebar highlights active page
- [ ] Breadcrumbs update on navigation
- [ ] Browser back/forward works correctly
- [ ] Fragments served for HX-Request headers

### Deployment (Phase 2)

| Method | Additions |
|--------|-----------|
| Docker Compose | NGINX `HX-Request` header detection, fragment routing |
| Helm Chart | Ingress annotations for fragment routing |
| Cloudflare | Worker routes fragments based on `HX-Request` header |

### Test Cases

```rust
#[test]
fn test_htmx_attributes() {
let html = render_page(&chapter).unwrap();
assert!(html.contains(r#"hx-boost="true""#));
assert!(html.contains(r#"hx-target="#content""#));
}

#[test]
fn test_fragment_output() {
let ctx = test_render_context();
render(&ctx).unwrap();
assert!(ctx.destination.join("fragments/chapter-1.html").exists());
let fragment = fs::read_to_string(...).unwrap();
assert!(!fragment.contains(""));
}
```

---

## Phase 3: Authorization (v0.3.0)

### Goal

Extract and surface authorization metadata for server-side enforcement.

### Deliverables

| Component | Description |
|-----------|-------------|
| Frontmatter parsing | Extract auth from YAML frontmatter |
| Manifest auth | Include auth requirements per page |
| Access-denied template | Partial for unauthorized access |
| Signin template | Partial for authentication prompt |
| Validation | Build-time auth config validation |

### Implementation Order

```
1. Frontmatter extractor
2. Auth schema validation
3. Manifest auth fields
4. Access-denied partial
5. Signin partial
6. CLI validation command
```

### Exit Criteria

- [ ] Frontmatter `auth:` section parsed correctly
- [ ] Manifest includes auth requirements per page
- [ ] Invalid auth config fails build with clear error
- [ ] Partials exist for access-denied and signin
- [ ] Reference server implementation documented

### Deployment (Phase 3)

| Method | Additions |
|--------|-----------|
| Docker Compose | `compose.auth.yml` overlay with OAuth2 proxy |
| Helm Chart | OAuth2 proxy sidecar, auth secret templates |
| Cloudflare | D1 database for sessions, auth middleware in worker |

Files:
- `docker/compose.auth.yml`
- `workers/auth.ts`, `workers/middleware.ts`
- `migrations/0001_sessions.sql`

### Test Cases

```rust
#[test]
fn test_frontmatter_auth() {
let (fm, _) = extract_frontmatter(r#"---
auth:
access: roles
roles: [admin]
---
# Content"#).unwrap();
assert_eq!(fm.auth.access, AccessLevel::Roles);
assert_eq!(fm.auth.roles, vec!["admin"]);
}

#[test]
fn test_manifest_includes_auth() {
let manifest = generate_manifest(&ctx).unwrap();
let admin_page = manifest.pages.iter()
.find(|p| p.path == "/docs/admin").unwrap();
assert_eq!(admin_page.auth.roles, vec!["admin"]);
}
```

---

## Phase 4: Search (v0.4.0)

### Goal

Generate search indexes for server-side search.

### Deliverables

| Component | Description |
|-----------|-------------|
| Index generation | Build search-index.json |
| Heading extraction | Index up to H3 by default |
| Body indexing | Full text content |
| Auth filtering | Include auth info for runtime filtering |
| Search partial | Results display template |

### Implementation Order

```
1. Document indexer
2. Heading extractor
3. Body text stripper (remove markdown)
4. JSON index writer
5. Search partial template
6. Index configuration options
```

### Exit Criteria

- [ ] search-index.json generated with all pages
- [ ] Headings indexed with anchors
- [ ] Body text stripped of markdown
- [ ] Auth info included for filtering
- [ ] Search partial renders results

### Deployment (Phase 4)

| Method | Additions |
|--------|-----------|
| Docker Compose | `compose.search.yml` overlay with Meilisearch, indexer |
| Helm Chart | Meilisearch StatefulSet, PVC, search init job |
| Cloudflare | KV namespace for search index, search handler |

Files:
- `docker/compose.search.yml`, `docker/init-meilisearch.sh`
- `workers/search.ts`, `scripts/upload-search-index.ts`

### Test Cases

```rust
#[test]
fn test_search_index() {
let index = generate_search_index(&ctx).unwrap();
assert!(!index.documents.is_empty());

let doc = &index.documents[0];
assert!(!doc.body.contains("```")); // No code blocks
assert!(doc.headings.iter().any(|h| h.level == 2));
}

#[test]
fn test_search_index_includes_auth() {
let index = generate_search_index(&ctx).unwrap();
let admin_doc = index.documents.iter()
.find(|d| d.path == "/docs/admin").unwrap();
assert_eq!(admin_doc.auth.access, "roles");
}
```

---

## Phase 5: Polish (v1.0.0)

### Goal

Production-ready features: theming, scopes, asset handling.

### Deliverables

| Component | Description |
|-----------|-------------|
| Scope filtering | Per-scope manifests and indexes |
| Asset hashing | Content-based cache busting |
| CSS variables | Theming system |
| Dark mode | Theme switching |
| Minification | Optional HTML/CSS minification |

### Implementation Order

```
1. Scope configuration
2. Scope-filtered manifest generation
3. Scope-filtered search indexes
4. Asset hasher
5. CSS variable system
6. Dark/light theme support
7. Minification (optional)
```

### Exit Criteria

- [ ] Scope-filtered manifests and indexes generated
- [ ] Asset filenames include content hash
- [ ] Theme switching works via CSS variables
- [ ] Dark mode respects system preference
- [ ] Performance benchmarks documented

### Deployment (Phase 5)

| Method | Additions |
|--------|-----------|
| Docker Compose | `compose.prod.yml` with caching, metrics |
| Helm Chart | HPA, PDB, ServiceMonitor for Prometheus |
| Cloudflare | R2 bucket for assets, analytics, cache optimization |

Files:
- `docker/compose.prod.yml`, `docker/nginx-cache.conf`
- `workers/analytics.ts`

### Test Cases

```rust
#[test]
fn test_scope_filtering() {
let ctx = test_render_context_with_scopes();
render(&ctx).unwrap();

let dev_manifest: Manifest = serde_json::from_reader(
File::open(ctx.destination.join("manifest.developers.json")).unwrap()
).unwrap();

assert!(dev_manifest.pages.iter().all(|p| p.scopes.contains(&"developers")));
}

#[test]
fn test_asset_hashing() {
let ctx = test_render_context();
render(&ctx).unwrap();

let css_files: Vec<_> = glob("book/htmx/assets/css/*.css").unwrap().collect();
assert!(css_files.iter().any(|f| f.contains("-"))); // Hash in filename
}
```

---

## Feature Flags

Each phase's features are gated behind configuration:

```toml
[output.htmx]
# Phase 1: Always on (core rendering)

# Phase 2: HTMX attributes
boost = true # Can disable for static output

# Phase 3: Authorization
[output.htmx.authz]
enabled = true

# Phase 4: Search
[output.htmx.search]
enabled = true

# Phase 5: Advanced features
[output.htmx.scopes]
enabled = true

[output.htmx.assets]
hash-files = true
```

---

## Dependency Graph

```
Phase 1 (Core)

Phase 2 (HTMX) ──────────────────┐
↓ │
Phase 3 (Auth) ────┐ │
↓ ↓ │
Phase 4 (Search) │ │
↓ │ │
Phase 5 (Polish) ←─┴─────────────┘
```

- Phase 2 depends on Phase 1 (needs basic rendering)
- Phase 3 depends on Phase 1 (needs manifest)
- Phase 4 depends on Phase 1 + 3 (needs auth for filtering)
- Phase 5 depends on all phases

---

## Consequences

### Positive

- Early releases provide value
- Each phase is testable in isolation
- Complex features don't block simpler ones
- User feedback informs later phases

### Negative

- Multiple releases to track
- Some refactoring between phases
- Users may need to upgrade incrementally

### Mitigation

- Semantic versioning with clear changelogs
- Migration guides between versions
- Feature flags for gradual adoption

## References

- [ADR-0012: MDBook Renderer Trait Implementation](./0012-mdbook-renderer-trait-implementation.md)
- [ADR-0003: Authorization Metadata via Manifest](./0003-authorization-via-manifest.md)
- [ADR-0005: Server-Side Search](./0005-server-side-search.md)
- [Incremental Delivery (Martin Fowler)](https://martinfowler.com/bliki/FrequencyReducesDifficulty.html)

### Deployment Examples

- [Docker Compose](../examples/docker-compose.md)
- [Helm Chart](../examples/helm-chart.md)
- [Cloudflare Pages Static](../examples/cf-pages-static.md)
- [Cloudflare Workers + D1](../examples/cf-workers-d1.md)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.