# 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)