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