Implements tag/category system backend following microformats2 p-category specification. Database changes: - Migration 008: Add tags and note_tags tables - Normalized tag storage (case-insensitive lookup, display name preserved) - Indexes for performance New module: - starpunk/tags.py: Tag management functions - normalize_tag: Normalize tag strings - get_or_create_tag: Get or create tag records - add_tags_to_note: Associate tags with notes (replaces existing) - get_note_tags: Retrieve note tags (alphabetically ordered) - get_tag_by_name: Lookup tag by normalized name - get_notes_by_tag: Get all notes with specific tag - parse_tag_input: Parse comma-separated tag input Model updates: - Note.tags property (lazy-loaded, prefer pre-loading in routes) - Note.to_dict() add include_tags parameter CRUD updates: - create_note() accepts tags parameter - update_note() accepts tags parameter (None = no change, [] = remove all) Micropub integration: - Pass tags to create_note() (tags already extracted by extract_tags()) - Return tags in q=source response Per design doc: docs/design/v1.3.0/microformats-tags-design.md Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
7.8 KiB
Custom Slug Bug Fix - Implementation Report
Date: 2025-11-25 Developer: StarPunk Developer Subagent Branch: bugfix/custom-slug-extraction Status: Complete - Ready for Testing
Executive Summary
Successfully fixed the custom slug extraction bug in the Micropub handler. Custom slugs specified via mp-slug parameter are now correctly extracted and used when creating notes.
Problem Statement
Custom slugs specified via the mp-slug property in Micropub requests were being completely ignored. The system was falling back to auto-generated slugs even when a custom slug was provided by the client (e.g., Quill).
Root Cause: mp-slug was being extracted from normalized properties after it had already been filtered out by normalize_properties() which removes all mp-* parameters.
Implementation Details
Files Modified
-
starpunk/micropub.py (lines 290-307)
- Moved
mp-slugextraction to BEFORE property normalization - Added support for both form-encoded and JSON request formats
- Added clear comments explaining the timing requirement
- Moved
-
tests/test_micropub.py (added lines 191-246)
- Added
test_micropub_create_with_custom_slug_form()- tests form-encoded requests - Added
test_micropub_create_with_custom_slug_json()- tests JSON requests - Both tests verify the custom slug is actually used in the created note
- Added
Code Changes
Before (Broken)
# Normalize and extract properties
try:
properties = normalize_properties(data) # mp-slug gets filtered here!
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 not in properties!
slug_values = properties.get('mp-slug', [])
if slug_values and len(slug_values) > 0:
custom_slug = slug_values[0]
After (Fixed)
# 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)
content = extract_content(properties)
title = extract_title(properties)
tags = extract_tags(properties)
published_date = extract_published_date(properties)
Why This Fix Works
- Extracts before filtering: Gets
mp-slugfrom raw request data beforenormalize_properties()filters it out - Handles both formats:
- Form-encoded:
mp-slugis a list["slug-value"] - JSON:
mp-slugcan be string"slug-value"or list["slug-value"]
- Form-encoded:
- Preserves existing flow: The
custom_slugvariable was already being passed tocreate_note()correctly - Architecturally correct: Treats
mp-slugas a server parameter (not a property), which aligns with Micropub spec
Test Results
Micropub Test Suite
All 13 Micropub tests passed:
tests/test_micropub.py::test_micropub_no_token PASSED
tests/test_micropub.py::test_micropub_invalid_token PASSED
tests/test_micropub.py::test_micropub_insufficient_scope PASSED
tests/test_micropub.py::test_micropub_create_note_form PASSED
tests/test_micropub.py::test_micropub_create_note_json PASSED
tests/test_micropub.py::test_micropub_create_with_name PASSED
tests/test_micropub.py::test_micropub_create_with_categories PASSED
tests/test_micropub.py::test_micropub_create_with_custom_slug_form PASSED # NEW
tests/test_micropub.py::test_micropub_create_with_custom_slug_json PASSED # NEW
tests/test_micropub.py::test_micropub_query_config PASSED
tests/test_micropub.py::test_micropub_query_source PASSED
tests/test_micropub.py::test_micropub_missing_content PASSED
tests/test_micropub.py::test_micropub_unsupported_action PASSED
New Test Coverage
Test 1: Form-encoded with custom slug
- Request:
POST /micropubwithcontent=...&mp-slug=my-custom-slug - Verifies: Location header ends with
/notes/my-custom-slug - Verifies: Note exists in database with correct slug
Test 2: JSON with custom slug
- Request:
POST /micropubwith JSON body including"mp-slug": "json-custom-slug" - Verifies: Location header ends with
/notes/json-custom-slug - Verifies: Note exists in database with correct slug
Regression Testing
All existing Micropub tests continue to pass, confirming:
- Authentication still works correctly
- Scope checking still works correctly
- Auto-generated slugs still work when no
mp-slugprovided - Content extraction still works correctly
- Title and category handling still works correctly
Validation Against Requirements
Per the architect's bug report (docs/reports/custom-slug-bug-diagnosis.md):
- Extract
mp-slugfrom raw request data - Extract BEFORE calling
normalize_properties() - Handle both form-encoded (list) and JSON (string or list) formats
- Pass
custom_slugtocreate_note() - Add tests for both request formats
- Ensure existing tests still pass
Architecture Compliance
The fix maintains architectural correctness:
- Separation of Concerns:
mp-slugis correctly treated as a server extension parameter, not a Micropub property - Existing Validation Pipeline: The slug still goes through all validation in
create_note():- Reserved slug checking
- Uniqueness checking with suffix generation if needed
- Sanitization
- No Breaking Changes: All existing functionality preserved
- Micropub Spec Compliance: Aligns with how
mp-*extensions should be handled
Deployment Notes
What to Test in Production
-
Create note with custom slug via Quill:
- Use Quill client to create a note
- Specify a custom slug in the slug field
- Verify the created note uses your specified slug
-
Create note without custom slug:
- Create a note without specifying a slug
- Verify auto-generation still works
-
Reserved slug handling:
- Try to create a note with slug "api" or "admin"
- Should be rejected with validation error
-
Duplicate slug handling:
- Create a note with slug "test-slug"
- Try to create another with the same slug
- Should get "test-slug-xxxx" with random suffix
Known Issues
None. The fix is clean and complete.
Version Impact
This fix will be included in v1.1.0-rc.2 (or next release).
Git Information
Branch: bugfix/custom-slug-extraction
Commit: 894e5e3
Commit Message: "fix: Extract mp-slug before property normalization"
Files Changed:
starpunk/micropub.py(69 insertions, 8 deletions)tests/test_micropub.py(added 2 comprehensive tests)
Next Steps
- Merge
bugfix/custom-slug-extractionintomain - Deploy to production
- Test with Quill client in production environment
- Update CHANGELOG.md with fix details
- Close any related issue tickets
References
- Bug Diagnosis:
/home/phil/Projects/starpunk/docs/reports/custom-slug-bug-diagnosis.md - Micropub Spec: https://www.w3.org/TR/micropub/
- Related ADR: ADR-029 (Micropub Property Mapping)
Conclusion
The custom slug feature is now fully functional. The bug was a simple timing issue in the extraction logic - trying to get mp-slug after it had been filtered out. The fix is clean, well-tested, and maintains all existing functionality while enabling the custom slug feature as originally designed.
The implementation follows the architect's design exactly and adds comprehensive test coverage for future regression prevention.