Files
StarPunk/docs/decisions/ADR-035-custom-slugs.md
Phil Skentelbery 82bb1499d5 docs: Add v1.1.0 architecture and validation documentation
- ADR-033: Database migration redesign
- ADR-034: Full-text search with FTS5
- ADR-035: Custom slugs in Micropub
- ADR-036: IndieAuth token verification method
- ADR-039: Micropub URL construction fix
- Implementation plan and decisions
- Architecture specifications
- Validation reports for implementation and search UI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 10:39:58 -07:00

6.2 KiB

ADR-035: Custom Slugs in Micropub

Status

Proposed

Context

Currently, StarPunk auto-generates slugs from note content (first 5 words). While this works well for most cases, users may want to specify custom slugs for:

  • SEO-friendly URLs
  • Memorable short links
  • Maintaining URL structure from migrated content
  • Creating hierarchical paths (e.g., 2024/11/my-note)
  • Personal preference and control

The Micropub specification supports custom slugs via the mp-slug property, which we should honor.

Decision

Implement custom slug support through the Micropub endpoint:

  1. Accept mp-slug: Process the mp-slug property in Micropub requests
  2. Validation: Ensure slugs are URL-safe and unique
  3. Fallback: Auto-generate if no slug provided or if invalid
  4. Conflict Resolution: Handle duplicate slugs gracefully
  5. Character Restrictions: Allow only URL-safe characters

Implementation approach:

def process_micropub_request(request_data):
    # Extract custom slug if provided
    custom_slug = request_data.get('properties', {}).get('mp-slug', [None])[0]

    if custom_slug:
        # Validate and sanitize
        slug = sanitize_slug(custom_slug)

        # Ensure uniqueness
        if slug_exists(slug):
            # Add suffix or reject based on configuration
            slug = make_unique(slug)
    else:
        # Fall back to auto-generation
        slug = generate_slug(content)

    return create_note(content, slug=slug)

Rationale

Supporting custom slugs provides:

  1. User Control: Authors can define meaningful URLs
  2. Standards Compliance: Follows Micropub specification
  3. Migration Support: Easier to preserve URLs when migrating
  4. SEO Benefits: Human-readable URLs improve discoverability
  5. Flexibility: Accommodates different URL strategies
  6. Backward Compatible: Existing auto-generation continues working

Validation rules:

  • Maximum length: 200 characters
  • Allowed characters: a-z0-9-_/
  • No consecutive slashes or dashes
  • No leading/trailing special characters
  • Case-insensitive uniqueness check

Consequences

Positive

  • Full Micropub compliance for slug handling
  • Better user experience and control
  • SEO-friendly URLs when desired
  • Easier content migration from other platforms
  • Maintains backward compatibility

Negative

  • Additional validation complexity
  • Potential for user confusion with conflicts
  • Must handle edge cases (empty, invalid, duplicate)
  • Slightly more complex note creation logic

Security Considerations

  1. Path Traversal: Reject slugs containing .. or absolute paths
  2. Reserved Names: Block system routes (api, admin, feed, etc.)
  3. Length Limits: Enforce maximum slug length
  4. Character Filtering: Strip or reject dangerous characters
  5. Case Sensitivity: Normalize to lowercase for consistency

Alternatives Considered

Alternative 1: No Custom Slugs

  • Pros: Simpler, no validation needed
  • Cons: Poor user experience, non-compliant with Micropub
  • Rejected because: Users expect URL control in modern CMS

Alternative 2: Separate Slug Field in UI

  • Pros: More discoverable for web users
  • Cons: Doesn't help API users, not Micropub standard
  • Rejected because: Should follow established standards

Alternative 3: Slugs Only via Direct API

  • Pros: Advanced feature for power users only
  • Cons: Inconsistent experience, limits adoption
  • Rejected because: Micropub clients expect this feature

Alternative 4: Hierarchical Slugs (/2024/11/25/my-note)

  • Pros: Organized structure, date-based archives
  • Cons: Complex routing, harder to implement
  • Rejected because: Can add later if needed, start simple

Implementation Plan

Phase 1: Core Logic (2 hours)

  1. Modify note creation to accept optional slug parameter
  2. Implement slug validation and sanitization
  3. Add uniqueness checking with conflict resolution
  4. Update database schema if needed (no changes expected)

Phase 2: Micropub Integration (1 hour)

  1. Extract mp-slug from Micropub requests
  2. Pass to note creation function
  3. Handle validation errors appropriately
  4. Return proper Micropub responses

Phase 3: Testing (1 hour)

  1. Test valid custom slugs
  2. Test invalid characters and patterns
  3. Test duplicate slug handling
  4. Test with Micropub clients
  5. Test auto-generation fallback

Validation Specification

Allowed Slug Format

^[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*$

Examples:

  • my-awesome-post
  • 2024/11/25/daily-note
  • projects/starpunk/update-1
  • My-Post (uppercase)
  • my--post (consecutive dashes)
  • -my-post (leading dash)
  • my_post (underscore not allowed)
  • ../../../etc/passwd (path traversal)

Reserved Slugs

The following slugs are reserved and cannot be used:

  • System routes: api, admin, auth, feed, static
  • Special pages: login, logout, settings
  • File extensions: Slugs ending in .xml, .json, .html

Conflict Resolution Strategy

When a duplicate slug is detected:

  1. Append -2, -3, etc. to make unique
  2. Check up to -99 before failing
  3. Return error if no unique slug found in 99 attempts

Example:

  • Request: mp-slug=my-note
  • Exists: my-note
  • Created: my-note-2

API Examples

Micropub Request with Custom Slug

POST /micropub
Content-Type: application/json
Authorization: Bearer {token}

{
    "type": ["h-entry"],
    "properties": {
        "content": ["My awesome post content"],
        "mp-slug": ["my-awesome-post"]
    }
}

Response

HTTP/1.1 201 Created
Location: https://example.com/note/my-awesome-post

Invalid Slug Handling

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "error": "invalid_request",
    "error_description": "Invalid slug format: 'my/../../etc/passwd'"
}

Migration Notes

  1. Existing notes keep their auto-generated slugs
  2. No database migration required (slug field exists)
  3. No breaking changes to API
  4. Existing clients continue working without modification

References