Federated content has no character limit — use the complete plaintext
body so followers see the full post in their timeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Switch object type from Article to Note (Mastodon first-class support)
- Include title + excerpt as HTML content with "Read more" link
- Feature image + up to 3 inline images as AP attachments
- Post tags as AP Hashtag objects with inline links in content
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace emit_event("post.published/updated/unpublished") with direct
try_publish() calls. AP activities are now created at write time,
fixing the race condition where multiple EventProcessors competed
for federation events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- _upsert_post returns (post, old_status) to detect status transitions
- Emit post.unpublished when published→draft (triggers Delete activity)
- Emit post.updated only when already-published posts are edited
- Emit post.published only for new publishes (not re-syncs)
- Same logic for pages via sync_single_page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
begin_nested() auto-flushes on entry which triggers the INSERT before
the savepoint is active. Move sess.add() inside the savepoint block
and split into update vs insert paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Blog app now registers SqlFederationService (was stub/no-op)
- sync_single_page emits post.published/updated events for pages
- Updated shared submodule: fix sign_request in AP delivery handler
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ghost /pages/ endpoint may not include "page": true in the response.
Explicitly set it so _upsert_post correctly marks is_page=true.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ghost has separate /posts/ and /pages/ endpoints. All admin functions
(get_post_for_edit, update_post, update_post_settings, sync) were
hardcoded to /posts/, causing 404s when editing pages. Now checks
is_page from post_data and uses the correct endpoint.
Also uses sync_single_page (not sync_single_post) after page saves
to prevent IntegrityError from mismatched fetch/upsert paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace all imports from cart.models, market.models, events.models
with shared.models equivalents
- Convert blog/models/ghost_content.py to re-export stub
- Update shared + glue submodule pointers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1-3 of decoupling:
- path_setup.py adds project root to sys.path
- Blog-owned models in blog/models/ (ghost_content, snippet, tag_group)
- Re-export shims for shared models (user, kv, magic_link, menu_item)
- All imports updated: shared.infrastructure, shared.db, shared.browser, etc.
- No more cross-app post_id FKs in calendar/market/page_config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add PageConfig model with feature flags (calendar, market)
- Auto-create PageConfig on Ghost page sync
- Add create_page() for Ghost /pages/ API endpoint
- Add /new-page/ route for creating pages
- Add ?type=pages blog filter with Posts|Pages tab toggle
- Add list_pages() to DBClient with PageConfig eager loading
- Add PUT /<slug>/admin/features/ route for feature toggles
- Add feature badges (calendar, market) on page cards
- Add features panel to page admin dashboard
- Update shared_lib submodule with PageConfig model
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract blog-specific code from the coop monolith into a standalone
repository. Includes auth, blog, post, admin, menu_items, snippets
blueprints, associated templates, Dockerfile (APP_MODULE=app:app),
entrypoint, and Gitea CI workflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>