Files
StarPunk/docs/reports/custom-slug-bug-diagnosis.md
Phil Skentelbery f28a48f560 docs: Update project plan for v1.1.0 completion
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>
2025-11-25 11:31:43 -07:00

7.5 KiB

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()

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

# 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):

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

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

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

    {
      "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.