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