- 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>
204 lines
6.2 KiB
Markdown
204 lines
6.2 KiB
Markdown
# 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:
|
|
```python
|
|
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
|
|
```regex
|
|
^[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
|
|
```http
|
|
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
|
|
HTTP/1.1 201 Created
|
|
Location: https://example.com/note/my-awesome-post
|
|
```
|
|
|
|
### Invalid Slug Handling
|
|
```http
|
|
HTTP/1.1 400 Bad Request
|
|
Content-Type: application/json
|
|
|
|
```
|
|
|
|
## 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
|
|
- Micropub Specification: https://www.w3.org/TR/micropub/#mp-slug
|
|
- URL Slug Best Practices: https://stackoverflow.com/questions/695438/safe-characters-for-friendly-url
|
|
- IndieWeb Slug Examples: https://indieweb.org/slug |