feat: Add custom slug support via mp-slug property
Implements custom slug handling for Micropub as specified in ADR-035. Changes: - Created starpunk/slug_utils.py with validation/sanitization functions - Added RESERVED_SLUGS constant (api, admin, auth, feed, etc.) - Modified create_note() to accept optional custom_slug parameter - Integrated mp-slug extraction in Micropub handle_create() - Slug sanitization: lowercase, hyphens, no special chars - Conflict resolution: sequential numbering (-2, -3, etc.) - Hierarchical slugs (/) rejected (deferred to v1.2.0) Features: - Custom slugs via Micropub's mp-slug property - Automatic sanitization of invalid characters - Reserved slug protection - Sequential conflict resolution (not random) - Clear error messages for validation failures Part of v1.1.0 (Phase 4). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -134,7 +134,7 @@ def _get_existing_slugs(db) -> set[str]:
|
||||
|
||||
|
||||
def create_note(
|
||||
content: str, published: bool = False, created_at: Optional[datetime] = None
|
||||
content: str, published: bool = False, created_at: Optional[datetime] = None, custom_slug: Optional[str] = None
|
||||
) -> Note:
|
||||
"""
|
||||
Create a new note
|
||||
@@ -147,6 +147,7 @@ def create_note(
|
||||
content: Markdown content for the note (must not be empty)
|
||||
published: Whether the note should be published (default: False)
|
||||
created_at: Creation timestamp (default: current UTC time)
|
||||
custom_slug: Optional custom slug (from Micropub mp-slug property)
|
||||
|
||||
Returns:
|
||||
Note object with all metadata and content loaded
|
||||
@@ -208,20 +209,27 @@ def create_note(
|
||||
|
||||
data_dir = Path(current_app.config["DATA_PATH"])
|
||||
|
||||
# 3. GENERATE UNIQUE SLUG
|
||||
# 3. GENERATE OR VALIDATE SLUG
|
||||
# Query all existing slugs from database
|
||||
db = get_db(current_app)
|
||||
existing_slugs = _get_existing_slugs(db)
|
||||
|
||||
# Generate base slug from content
|
||||
base_slug = generate_slug(content, created_at)
|
||||
if custom_slug:
|
||||
# Use custom slug (from Micropub mp-slug property)
|
||||
from starpunk.slug_utils import validate_and_sanitize_custom_slug
|
||||
success, slug, error = validate_and_sanitize_custom_slug(custom_slug, existing_slugs)
|
||||
if not success:
|
||||
raise InvalidNoteDataError("slug", custom_slug, error)
|
||||
else:
|
||||
# Generate base slug from content
|
||||
base_slug = generate_slug(content, created_at)
|
||||
|
||||
# Make unique if collision
|
||||
slug = make_slug_unique(base_slug, existing_slugs)
|
||||
# Make unique if collision
|
||||
slug = make_slug_unique(base_slug, existing_slugs)
|
||||
|
||||
# Validate final slug (defensive check)
|
||||
if not validate_slug(slug):
|
||||
raise InvalidNoteDataError("slug", slug, f"Generated slug is invalid: {slug}")
|
||||
# Validate final slug (defensive check)
|
||||
if not validate_slug(slug):
|
||||
raise InvalidNoteDataError("slug", slug, f"Generated slug is invalid: {slug}")
|
||||
|
||||
# 4. GENERATE FILE PATH
|
||||
note_path = generate_note_path(slug, created_at, data_dir)
|
||||
|
||||
Reference in New Issue
Block a user