docs: v1.5.0 planning - ADR-062, release plan, and design docs
- ADR-062: Timestamp-based slug format (supersedes ADR-007) - Updated v1.5.0 RELEASE.md with 6-phase plan - Updated BACKLOG.md with deferred N+1 query locations - Developer questions and architect responses for Phase 1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
197
docs/decisions/ADR-062-timestamp-based-slug-format.md
Normal file
197
docs/decisions/ADR-062-timestamp-based-slug-format.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# ADR-062: Timestamp-Based Slug Format
|
||||
|
||||
## Status
|
||||
Accepted (Supersedes ADR-007)
|
||||
|
||||
## Context
|
||||
|
||||
ADR-007 established a content-based slug generation algorithm that extracts the first 5 words from note content to create URL slugs. While this approach provides readable, SEO-friendly URLs, it has drawbacks for a personal note-taking system:
|
||||
|
||||
1. **Privacy Concerns**: The slug reveals note content in the URL. Private thoughts or draft content become visible in URLs that may be shared or logged.
|
||||
|
||||
2. **SEO Irrelevance**: StarPunk is designed for personal IndieWeb notes, not public blogs. Notes are typically short-form content (similar to tweets or status updates) where SEO optimization provides no meaningful benefit.
|
||||
|
||||
3. **Unpredictable Slugs**: Users cannot predict what slug will be generated without examining their content carefully.
|
||||
|
||||
4. **Edge Case Handling**: Content-based slugs require complex fallback logic for short content, unicode-only content, or special characters.
|
||||
|
||||
5. **Collision Complexity**: Similar notes require random suffix generation (e.g., `hello-world-a7c9`), which undermines the readability goal.
|
||||
|
||||
The user has explicitly stated: "Notes don't need SEO."
|
||||
|
||||
## Decision
|
||||
|
||||
Change the default slug format from content-based to timestamp-based:
|
||||
|
||||
**New Default Format**: `YYYYMMDDHHMMSS`
|
||||
- Example: `20251216143052`
|
||||
- Compact, sortable, predictable
|
||||
- 14 characters total
|
||||
|
||||
**Collision Handling**: Sequential numeric suffix
|
||||
- First collision: `20251216143052-1`
|
||||
- Second collision: `20251216143052-2`
|
||||
- Simple, predictable, no randomness
|
||||
|
||||
**Custom Slugs**: Continue to support user-specified slugs via `mp-slug` Micropub property and the web UI custom slug field. When provided, custom slugs take precedence.
|
||||
|
||||
### Algorithm Specification
|
||||
|
||||
```python
|
||||
def generate_slug(custom_slug: str = None, created_at: datetime = None) -> str:
|
||||
"""Generate a URL-safe slug for a note.
|
||||
|
||||
Args:
|
||||
custom_slug: User-provided custom slug (takes precedence if provided)
|
||||
created_at: Note creation timestamp (defaults to now)
|
||||
|
||||
Returns:
|
||||
URL-safe slug string
|
||||
"""
|
||||
if custom_slug:
|
||||
return sanitize_slug(custom_slug)
|
||||
|
||||
# Default: timestamp-based
|
||||
timestamp = (created_at or datetime.now()).strftime("%Y%m%d%H%M%S")
|
||||
return ensure_unique_slug(timestamp)
|
||||
|
||||
def ensure_unique_slug(base_slug: str) -> str:
|
||||
"""Ensure slug is unique, adding numeric suffix if needed."""
|
||||
if not slug_exists(base_slug):
|
||||
return base_slug
|
||||
|
||||
suffix = 1
|
||||
while slug_exists(f"{base_slug}-{suffix}"):
|
||||
suffix += 1
|
||||
return f"{base_slug}-{suffix}"
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
| Scenario | Generated Slug |
|
||||
|----------|----------------|
|
||||
| Normal note at 2:30:52 PM on Dec 16, 2025 | `20251216143052` |
|
||||
| Second note in same second | `20251216143052-1` |
|
||||
| Third note in same second | `20251216143052-2` |
|
||||
| Note with custom slug "my-custom-slug" | `my-custom-slug` |
|
||||
|
||||
## Rationale
|
||||
|
||||
### Timestamp Format (Score: 9/10)
|
||||
|
||||
**Pros**:
|
||||
- **Privacy**: URL reveals nothing about content
|
||||
- **Predictability**: User knows exactly what slug format to expect
|
||||
- **Sortability**: Chronological sorting by URL is possible
|
||||
- **Simplicity**: No complex word extraction or normalization
|
||||
- **Collision Rarity**: Same-second creation is rare; handled cleanly when it occurs
|
||||
- **Compact**: 14 characters vs potentially 50+ for content-based
|
||||
|
||||
**Cons**:
|
||||
- **Not Memorable**: `20251216143052` is harder to remember than `hello-world`
|
||||
- **No SEO Value**: Search engines prefer descriptive URLs
|
||||
|
||||
**Why Cons Don't Matter**:
|
||||
- StarPunk is for personal notes, not public SEO-optimized content
|
||||
- Notes are accessed via UI, feeds, or bookmarks, not memorized URLs
|
||||
- Users wanting memorable URLs can use custom slugs
|
||||
|
||||
### Sequential Suffix (Score: 10/10)
|
||||
|
||||
**Pros**:
|
||||
- **Deterministic**: No randomness; same collision always gets same suffix
|
||||
- **Simple**: No cryptographic random generation needed
|
||||
- **Readable**: `-1`, `-2` are clear and obvious
|
||||
- **Debuggable**: Easy to understand collision resolution
|
||||
|
||||
**Cons**:
|
||||
- **Enumerable**: Sequential numbers could be probed
|
||||
- Not a real security concern for note slugs
|
||||
|
||||
### Comparison with ADR-007 Approach
|
||||
|
||||
| Aspect | ADR-007 (Content-Based) | ADR-062 (Timestamp) |
|
||||
|--------|------------------------|---------------------|
|
||||
| Privacy | Reveals content | Reveals only time |
|
||||
| Complexity | High (word extraction, normalization, unicode handling) | Low (strftime) |
|
||||
| SEO | Good | None |
|
||||
| Predictability | Low | High |
|
||||
| Collision handling | Random suffix | Sequential suffix |
|
||||
| Fallback cases | Many (short content, unicode, etc.) | None |
|
||||
| Code lines | ~50 | ~15 |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Simplified Code**: Remove complex word extraction, unicode normalization, and multiple fallback paths
|
||||
2. **Better Privacy**: Note content never appears in URLs
|
||||
3. **Predictable Output**: Users always know what slug format to expect
|
||||
4. **Fewer Edge Cases**: No special handling for short content, unicode, or special characters
|
||||
5. **Cleaner Collisions**: Sequential suffixes are more intuitive than random strings
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Migration**: Existing notes keep their content-based slugs (no migration needed)
|
||||
2. **Not Human-Readable**: URLs don't describe content
|
||||
3. **No SEO**: Search engines won't benefit from descriptive URLs
|
||||
|
||||
### Mitigations
|
||||
|
||||
**Human-Readable URLs**:
|
||||
- Users wanting descriptive URLs can use custom slugs via `mp-slug`
|
||||
- The web UI custom slug field remains available
|
||||
- This is opt-in rather than default
|
||||
|
||||
**SEO**:
|
||||
- IndieWeb notes are typically not SEO targets
|
||||
- Content is in the page body where search engines can index it
|
||||
- Microformats2 markup provides semantic meaning
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
- Existing notes retain their slugs (no data migration)
|
||||
- New notes use timestamp format by default
|
||||
- Custom slug functionality unchanged
|
||||
- All existing URLs remain valid
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- Test default slug generation produces timestamp format
|
||||
- Test collision handling with sequential suffixes
|
||||
- Test custom slugs still take precedence
|
||||
- Test edge case: multiple notes in same second
|
||||
- Test reserved slug rejection still works
|
||||
- Verify existing tests for custom slugs pass
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Changes required in:
|
||||
- `starpunk/slug_utils.py`: Update `generate_slug()` function
|
||||
- `starpunk/notes.py`: Remove content parameter from slug generation call
|
||||
- Tests: Update expected slug formats
|
||||
|
||||
Estimated effort: Small (1-2 hours implementation, 1 hour testing)
|
||||
|
||||
## References
|
||||
|
||||
- ADR-007: Slug Generation Algorithm (Superseded by this ADR)
|
||||
- ADR-035: Custom Slugs (Unchanged; complements this decision)
|
||||
- IndieWeb Permalink Best Practices: https://indieweb.org/permalink
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Default slugs use `YYYYMMDDHHMMSS` format
|
||||
- [ ] Collision handling uses sequential suffix (`-1`, `-2`, etc.)
|
||||
- [ ] Custom slugs via `mp-slug` continue to work
|
||||
- [ ] Custom slugs via web UI continue to work
|
||||
- [ ] Reserved slug validation unchanged
|
||||
- [ ] Existing notes unaffected
|
||||
- [ ] All tests pass
|
||||
- [ ] Code complexity reduced
|
||||
|
||||
---
|
||||
|
||||
**Approved**: 2025-12-16
|
||||
**Architect**: StarPunk Architect Agent
|
||||
**Supersedes**: ADR-007 (Slug Generation Algorithm)
|
||||
Reference in New Issue
Block a user