Comprehensive project plan updates to reflect v1.1.0 release: New Documents: - INDEX.md: Navigation index for all planning docs - ROADMAP.md: Future version planning (v1.1.1 → v2.0.0) - v1.1/RELEASE-STATUS.md: Complete v1.1.0 tracking Updated Documents: - v1/implementation-plan.md: Updated to v1.1.0, marked V1 100% complete - v1.1/priority-work.md: Marked all items complete with actual effort Changes: - Fixed outdated status (was showing v0.9.5) - Marked Micropub as complete (v1.0.0) - Tracked all v1.1.0 features (search, slugs, migrations) - Added clear roadmap for future versions - Linked all ADRs and implementation reports Project plan now fully synchronized with v1.1.0 "SearchLight" release. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
231 lines
7.5 KiB
Markdown
231 lines
7.5 KiB
Markdown
# Custom Slug Bug Diagnosis Report
|
|
|
|
**Date**: 2025-11-25
|
|
**Issue**: Custom slugs (mp-slug) not working in production
|
|
**Architect**: StarPunk Architect Subagent
|
|
|
|
## Executive Summary
|
|
|
|
Custom slugs specified via the `mp-slug` property in Micropub requests are being completely ignored in production. The root cause is that `mp-slug` is being incorrectly extracted from the normalized properties dictionary instead of directly from the raw request data.
|
|
|
|
## Problem Reproduction
|
|
|
|
### Input
|
|
- **Client**: Quill (Micropub client)
|
|
- **Request Type**: Form-encoded POST to `/micropub`
|
|
- **Content**: "This is a test for custom slugs. Only the best slugs to be found here"
|
|
- **mp-slug**: "slug-test"
|
|
|
|
### Expected Result
|
|
- Note created with slug: `slug-test`
|
|
|
|
### Actual Result
|
|
- Note created with auto-generated slug: `this-is-a-test-for-f0x5`
|
|
- Redirect URL: `https://starpunk.thesatelliteoflove.com/notes/this-is-a-test-for-f0x5`
|
|
|
|
## Root Cause Analysis
|
|
|
|
### The Bug Location
|
|
|
|
**File**: `/home/phil/Projects/starpunk/starpunk/micropub.py`
|
|
**Lines**: 299-304
|
|
**Function**: `handle_create()`
|
|
|
|
```python
|
|
# Extract custom slug if provided (Micropub extension)
|
|
custom_slug = None
|
|
if 'mp-slug' in properties:
|
|
# mp-slug is an array in Micropub format
|
|
slug_values = properties.get('mp-slug', [])
|
|
if slug_values and len(slug_values) > 0:
|
|
custom_slug = slug_values[0]
|
|
```
|
|
|
|
### Why It's Broken
|
|
|
|
The code is looking for `mp-slug` in the `properties` dictionary, but `mp-slug` is **NOT** a property—it's a Micropub server extension parameter. The `normalize_properties()` function explicitly **EXCLUDES** all parameters that start with `mp-` from the properties dictionary.
|
|
|
|
Looking at line 139 in `micropub.py`:
|
|
```python
|
|
# Skip reserved Micropub parameters
|
|
if key.startswith("mp-") or key in ["action", "url", "access_token", "h"]:
|
|
continue
|
|
```
|
|
|
|
This means `mp-slug` is being filtered out before it ever reaches the properties dictionary!
|
|
|
|
## Data Flow Analysis
|
|
|
|
### Current (Broken) Flow
|
|
|
|
1. **Form-encoded request arrives** with `mp-slug=slug-test`
|
|
2. **Raw data parsed** in `micropub_endpoint()` (lines 97-99):
|
|
```python
|
|
data = request.form.to_dict(flat=False)
|
|
# data = {"content": ["..."], "mp-slug": ["slug-test"], ...}
|
|
```
|
|
|
|
3. **Data passed to `handle_create()`** (line 103)
|
|
|
|
4. **Properties normalized** via `normalize_properties()` (line 292):
|
|
- Line 139 **SKIPS** `mp-slug` because it starts with "mp-"
|
|
- Result: `properties = {"content": ["..."]}`
|
|
- `mp-slug` is LOST!
|
|
|
|
5. **Attempt to extract mp-slug** (lines 299-304):
|
|
- Looks for `mp-slug` in properties
|
|
- Never finds it (was filtered out)
|
|
- `custom_slug` remains `None`
|
|
|
|
6. **Note created** with `custom_slug=None` (line 318)
|
|
- Falls back to auto-generated slug
|
|
|
|
### Correct Flow (How It Should Work)
|
|
|
|
1. Form-encoded request arrives with `mp-slug=slug-test`
|
|
2. Raw data parsed
|
|
3. Data passed to `handle_create()`
|
|
4. Extract `mp-slug` **BEFORE** normalizing properties:
|
|
```python
|
|
# Extract mp-slug from raw data (before normalization)
|
|
custom_slug = None
|
|
if isinstance(data, dict):
|
|
if 'mp-slug' in data:
|
|
slug_values = data.get('mp-slug', [])
|
|
if isinstance(slug_values, list) and slug_values:
|
|
custom_slug = slug_values[0]
|
|
elif isinstance(slug_values, str):
|
|
custom_slug = slug_values
|
|
```
|
|
5. Normalize properties (mp-slug gets filtered, which is correct)
|
|
6. Pass `custom_slug` to `create_note()`
|
|
|
|
## The Fix
|
|
|
|
### Required Code Changes
|
|
|
|
**File**: `/home/phil/Projects/starpunk/starpunk/micropub.py`
|
|
**Function**: `handle_create()`
|
|
**Lines to modify**: 289-305
|
|
|
|
Replace the current implementation:
|
|
```python
|
|
# Normalize and extract properties
|
|
try:
|
|
properties = normalize_properties(data)
|
|
content = extract_content(properties)
|
|
title = extract_title(properties)
|
|
tags = extract_tags(properties)
|
|
published_date = extract_published_date(properties)
|
|
|
|
# Extract custom slug if provided (Micropub extension)
|
|
custom_slug = None
|
|
if 'mp-slug' in properties: # BUG: mp-slug is not in properties!
|
|
# mp-slug is an array in Micropub format
|
|
slug_values = properties.get('mp-slug', [])
|
|
if slug_values and len(slug_values) > 0:
|
|
custom_slug = slug_values[0]
|
|
```
|
|
|
|
With the corrected implementation:
|
|
```python
|
|
# Extract mp-slug BEFORE normalizing properties (it's not a property!)
|
|
custom_slug = None
|
|
if isinstance(data, dict) and 'mp-slug' in data:
|
|
# Handle both form-encoded (list) and JSON (could be string or list)
|
|
slug_value = data.get('mp-slug')
|
|
if isinstance(slug_value, list) and slug_value:
|
|
custom_slug = slug_value[0]
|
|
elif isinstance(slug_value, str):
|
|
custom_slug = slug_value
|
|
|
|
# Normalize and extract properties
|
|
try:
|
|
properties = normalize_properties(data)
|
|
content = extract_content(properties)
|
|
title = extract_title(properties)
|
|
tags = extract_tags(properties)
|
|
published_date = extract_published_date(properties)
|
|
```
|
|
|
|
### Why This Fix Works
|
|
|
|
1. **Extracts mp-slug from raw data** before normalization filters it out
|
|
2. **Handles both formats**:
|
|
- Form-encoded: `mp-slug` is a list `["slug-test"]`
|
|
- JSON: `mp-slug` could be string or list
|
|
3. **Preserves the custom slug** through to `create_note()`
|
|
4. **Maintains separation**: mp-slug is correctly treated as a server parameter, not a property
|
|
|
|
## Validation Strategy
|
|
|
|
### Test Cases
|
|
|
|
1. **Form-encoded with mp-slug**:
|
|
```
|
|
POST /micropub
|
|
Content-Type: application/x-www-form-urlencoded
|
|
|
|
content=Test+post&mp-slug=custom-slug
|
|
```
|
|
Expected: Note created with slug "custom-slug"
|
|
|
|
2. **JSON with mp-slug**:
|
|
```json
|
|
{
|
|
"type": ["h-entry"],
|
|
"properties": {
|
|
"content": ["Test post"]
|
|
},
|
|
"mp-slug": "custom-slug"
|
|
}
|
|
```
|
|
Expected: Note created with slug "custom-slug"
|
|
|
|
3. **Without mp-slug**:
|
|
Should auto-generate slug from content
|
|
|
|
4. **Reserved slug**:
|
|
mp-slug="api" should be rejected
|
|
|
|
5. **Duplicate slug**:
|
|
Should make unique with suffix
|
|
|
|
### Verification Steps
|
|
|
|
1. Apply the fix to `micropub.py`
|
|
2. Test with Quill client specifying custom slug
|
|
3. Verify slug matches the specified value
|
|
4. Check database to confirm correct slug storage
|
|
5. Test all edge cases above
|
|
|
|
## Architectural Considerations
|
|
|
|
### Design Validation
|
|
|
|
The current architecture is sound:
|
|
- Separation between Micropub parameters and properties is correct
|
|
- Slug validation pipeline in `slug_utils.py` is well-designed
|
|
- `create_note()` correctly accepts `custom_slug` parameter
|
|
|
|
The bug was purely an implementation error, not an architectural flaw.
|
|
|
|
### Standards Compliance
|
|
|
|
Per the Micropub specification:
|
|
- `mp-slug` is a server extension, not a property
|
|
- It should be extracted from the request, not from properties
|
|
- The fix aligns with Micropub spec requirements
|
|
|
|
## Recommendations
|
|
|
|
1. **Immediate Action**: Apply the fix to `handle_create()` function
|
|
2. **Add Tests**: Create unit tests for mp-slug extraction
|
|
3. **Documentation**: Update implementation notes to clarify mp-slug handling
|
|
4. **Code Review**: Check for similar parameter/property confusion elsewhere
|
|
|
|
## Conclusion
|
|
|
|
The custom slug feature is architecturally complete and correctly designed. The bug is a simple implementation error where `mp-slug` is being looked for in the wrong place. The fix is straightforward: extract `mp-slug` from the raw request data before it gets filtered out by the property normalization process.
|
|
|
|
This is a classic case of correct design with incorrect implementation—the kind of bug that's invisible in code review but immediately apparent in production use. |