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>
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
-
Form-encoded request arrives with
mp-slug=slug-test -
Raw data parsed in
micropub_endpoint()(lines 97-99):data = request.form.to_dict(flat=False) # data = {"content": ["..."], "mp-slug": ["slug-test"], ...} -
Data passed to
handle_create()(line 103) -
Properties normalized via
normalize_properties()(line 292):- Line 139 SKIPS
mp-slugbecause it starts with "mp-" - Result:
properties = {"content": ["..."]} mp-slugis LOST!
- Line 139 SKIPS
-
Attempt to extract mp-slug (lines 299-304):
- Looks for
mp-slugin properties - Never finds it (was filtered out)
custom_slugremainsNone
- Looks for
-
Note created with
custom_slug=None(line 318)- Falls back to auto-generated slug
Correct Flow (How It Should Work)
- Form-encoded request arrives with
mp-slug=slug-test - Raw data parsed
- Data passed to
handle_create() - Extract
mp-slugBEFORE 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 - Normalize properties (mp-slug gets filtered, which is correct)
- Pass
custom_slugtocreate_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
- Extracts mp-slug from raw data before normalization filters it out
- Handles both formats:
- Form-encoded:
mp-slugis a list["slug-test"] - JSON:
mp-slugcould be string or list
- Form-encoded:
- Preserves the custom slug through to
create_note() - Maintains separation: mp-slug is correctly treated as a server parameter, not a property
Validation Strategy
Test Cases
-
Form-encoded with mp-slug:
POST /micropub Content-Type: application/x-www-form-urlencoded content=Test+post&mp-slug=custom-slugExpected: Note created with slug "custom-slug"
-
JSON with mp-slug:
{ "type": ["h-entry"], "properties": { "content": ["Test post"] }, "mp-slug": "custom-slug" }Expected: Note created with slug "custom-slug"
-
Without mp-slug: Should auto-generate slug from content
-
Reserved slug: mp-slug="api" should be rejected
-
Duplicate slug: Should make unique with suffix
Verification Steps
- Apply the fix to
micropub.py - Test with Quill client specifying custom slug
- Verify slug matches the specified value
- Check database to confirm correct slug storage
- 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.pyis well-designed create_note()correctly acceptscustom_slugparameter
The bug was purely an implementation error, not an architectural flaw.
Standards Compliance
Per the Micropub specification:
mp-slugis a server extension, not a property- It should be extracted from the request, not from properties
- The fix aligns with Micropub spec requirements
Recommendations
- Immediate Action: Apply the fix to
handle_create()function - Add Tests: Create unit tests for mp-slug extraction
- Documentation: Update implementation notes to clarify mp-slug handling
- 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.