From 894e5e390671528f6d4c20d9fe73fafa64427924 Mon Sep 17 00:00:00 2001 From: Phil Skentelbery Date: Tue, 25 Nov 2025 11:03:28 -0700 Subject: [PATCH] fix: Extract mp-slug before property normalization Fix bug where custom slugs (mp-slug) were being ignored because they were extracted from normalized properties after being filtered out. The root cause: normalize_properties() filters out all mp-* parameters (line 139) because they're Micropub server extensions, not properties. The old code tried to extract mp-slug from the normalized properties dict, but it had already been removed. The fix: Extract mp-slug directly from raw request data BEFORE calling normalize_properties(). This preserves the custom slug through to create_note(). Changes: - Move mp-slug extraction to before property normalization (line 290-299) - Handle both form-encoded (list) and JSON (string or list) formats - Add comprehensive tests for custom slug with both request formats - All 13 Micropub tests pass Fixes the issue reported in production where Quill-specified slugs were being replaced with auto-generated ones. References: - docs/reports/custom-slug-bug-diagnosis.md (architect's analysis) - Micropub spec: mp-slug is a server extension parameter Generated with Claude Code Co-Authored-By: Claude --- starpunk/micropub.py | 19 ++++++++------ tests/test_micropub.py | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/starpunk/micropub.py b/starpunk/micropub.py index 2bb05c0..0e2a4e8 100644 --- a/starpunk/micropub.py +++ b/starpunk/micropub.py @@ -287,6 +287,17 @@ def handle_create(data: dict, token_info: dict): "insufficient_scope", "Token lacks create scope", status_code=403 ) + # Extract mp-slug BEFORE normalizing properties (it's not a property!) + # mp-slug is a Micropub server extension parameter that gets filtered during normalization + 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) @@ -295,14 +306,6 @@ def handle_create(data: dict, token_info: dict): 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: - # 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] - except MicropubValidationError as e: raise e except Exception as e: diff --git a/tests/test_micropub.py b/tests/test_micropub.py index 45f712b..7cf482c 100644 --- a/tests/test_micropub.py +++ b/tests/test_micropub.py @@ -188,6 +188,64 @@ def test_micropub_create_with_categories(client, app, mock_valid_token): assert 'Location' in response.headers +def test_micropub_create_with_custom_slug_form(client, app, mock_valid_token): + """Test creating a note with custom slug via form-encoded request""" + with patch('starpunk.routes.micropub.verify_external_token', mock_valid_token): + response = client.post( + '/micropub', + data={ + 'h': 'entry', + 'content': 'This is a test for custom slugs', + 'mp-slug': 'my-custom-slug' + }, + headers={'Authorization': 'Bearer valid_token'} + ) + + assert response.status_code == 201 + assert 'Location' in response.headers + + # Verify the custom slug was used + location = response.headers['Location'] + assert location.endswith('/notes/my-custom-slug') + + # Verify note exists with the custom slug + with app.app_context(): + note = get_note('my-custom-slug') + assert note is not None + assert note.slug == 'my-custom-slug' + assert note.content == 'This is a test for custom slugs' + + +def test_micropub_create_with_custom_slug_json(client, app, mock_valid_token): + """Test creating a note with custom slug via JSON request""" + with patch('starpunk.routes.micropub.verify_external_token', mock_valid_token): + response = client.post( + '/micropub', + json={ + 'type': ['h-entry'], + 'properties': { + 'content': ['JSON test with custom slug'] + }, + 'mp-slug': 'json-custom-slug' + }, + headers={'Authorization': 'Bearer valid_token'} + ) + + assert response.status_code == 201 + assert 'Location' in response.headers + + # Verify the custom slug was used + location = response.headers['Location'] + assert location.endswith('/notes/json-custom-slug') + + # Verify note exists with the custom slug + with app.app_context(): + note = get_note('json-custom-slug') + assert note is not None + assert note.slug == 'json-custom-slug' + assert note.content == 'JSON test with custom slug' + + # Query Tests