logicspike/docs

Blog Engine

Blog Service — Backlog: Issues, Gaps & Improvement Scope

Last Updated: 2026-06-28 Status: Active Purpose: Durable record of known bugs, missing features, and improvement opportunities found during codebase analysis. Update this doc as items are fixed or new issues are found. Do not delete fixed items — mark them ✅ and add the date.


Scorecard

Audited 2026-06-28 against apps/blog-service source. Overall: 7.4 / 10

The core — API design, data model, multi-tenancy, security — is production-grade. Two entire feature surfaces (scheduled auto-publish, analytics) are non-functional and drag the overall score down significantly. Fixing B4 (cron trigger) and G2 (analytics pipeline) would push this to ~8.8.

Parameter Score Notes
API Design 9.0 Clean admin/public/MCP split, consistent errors, cursor+page pagination
Data Model 9.0 5 tables, 8+ indexes, soft-delete, SWR cache, partial unique indexes
Security & Auth 9.5 Gateway secret guard, 7 permission scopes, query-level tenant isolation
Multi-tenancy 10.0 Perfect — tenant_id on every table and every query, requireTenant middleware
Billing 8.0 creditGuard + grace window + idempotency is solid; MCP unpublish skips refund (B2)
Content Pipeline 8.0 HTML cache + renderer versioning + lowlight tokenization; carousel missing (B1)
Scheduling 4.0 Data stored correctly; no cron trigger exists — posts stay scheduled forever (B4)
Analytics 2.5 Binding declared, metric emits are no-ops, get-analytics returns zeros (B5, G2)
Newsletter Integration 7.0 REST publish fans out; MCP skips it (B3); no retry/DLQ (B7)
MCP Tools 8.5 17 tools, full CRUD; behavioral divergence from REST undocumented (G4)
Testing 9.0 14 test files, all major flows covered; minor creditGuard edge case gaps
Observability 6.5 Slow-request logger present; analytics not wired; METRICS absent from local dev
Feature Completeness 7.5 Full CRUD + schedule + soft-delete; no analytics, no full-text admin search (G6)
Slug Management 5.5 Collision retry is good; no redirect table — old URLs 404 permanently (B6, G3)

Bugs (Broken Right Now)

These are things that are live and wrong. Fix before shipping new features.

Where: apps/blog-service/src/utils/tiptap-renderer.ts

tiptap-renderer.ts does not emit a <div data-type="carousel"> wrapper around carousel children. The @vlozi/blog SDK expects this wrapper to mount its carousel runtime — without it, posts with carousels render as a flat stack of <figure> elements. Any tenant who has inserted a carousel node has silently broken public posts.

Fix: Add carousel wrapper emission in tiptap-renderer.ts. Emit <div data-type="carousel"> around the rendered children when node type is carousel.


B2 — MCP unpublish-post silently skips 30-min credit refund HIGH

Where: apps/blog-service/src/routes/mcp/

The REST POST /admin/posts/:id/unpublish handler checks the 30-minute grace window and issues a credit refund if eligible. The MCP unpublish-post tool sets status=draft but never calls the grace-window refund path. Tenants unpublishing via MCP (agents, Cursor, Claude) permanently lose the credit they're entitled to. No warning is surfaced in the tool response.

Fix: Extract the grace-window refund logic into a shared function and call it from both the REST handler and the MCP tool. Or pipe MCP calls through the same handler.


B3 — MCP publish-post silently skips newsletter dispatch MEDIUM

Where: apps/blog-service/src/routes/mcp/

The REST POST /admin/posts/:id/publish fans out to newsletter-service via waitUntil(). The MCP publish-post tool charges the credit and flips status=published but never calls newsletter-service. Subscribers don't get notified. No warning is shown to the caller or in the tool schema description.

Fix (option A): Add waitUntil(notifyNewsletter(...)) to the MCP publish handler. Fix (option B): Add an explicit skipNewsletter?: boolean param to the tool and document the default behavior clearly.


B4 — Schedule → auto-publish is permanently half-built MEDIUM

Where: apps/blog-service/src/routes/admin/posts.ts, wrangler.toml

POST /:id/schedule correctly sets scheduled_for and status=scheduled. However, there is no Cloudflare Cron Trigger or background worker to flip those posts to published at the target time. Scheduled posts stay status=scheduled forever unless manually published. The feature is broken-by-default — tenants can set a schedule time, see it in the UI, and nothing happens.

Fix:

  1. Add a Cloudflare Cron Trigger to wrangler.toml (e.g., crons = ["* * * * *"] for minute-level resolution or "0 * * * *" for hourly).
  2. Add an /internal/publish-scheduled handler that queries WHERE status='scheduled' AND scheduled_for <= NOW() and publishes each post (including newsletter fan-out and credit charge).
  3. Secure the internal endpoint with INTERNAL_KEY check (same pattern as other internal routes).

B5 — get-analytics MCP tool returns hardcoded zeros MEDIUM

Where: apps/blog-service/src/routes/mcp/

get-analytics returns { collected: false, views: 0, reads: 0 } for every post. The analytics ingestion pipeline does not exist. Any agent or AI workflow reading this data makes decisions on fabricated zeros.

Fix: See G2 (Analytics gap) below. Until the pipeline is built, the tool response should be explicit: { available: false, reason: "analytics_not_configured" } rather than numeric zeros.


B6 — Slug breaks permanently on title update LOW

Where: apps/blog-service/src/routes/admin/posts.ts, post.utils.ts

When a post title is updated, a new slug is generated. The old slug is abandoned with no redirect row written. Old URLs (bookmarks, search index results, backlinks) return 404 permanently.

Fix: See G3 (Slug redirect table) below.


B7 — Newsletter fan-out has no retry or DLQ LOW

Where: apps/blog-service/src/routes/admin/posts.ts publish handler

The waitUntil() call to newsletter-service is fire-and-forget. A transient error drops the newsletter silently. No failure is surfaced to the publisher and no retry is attempted. Subscribers don't know they missed a dispatch.

Fix: See P2 (Newsletter retry) in Improvement Scope below.


B8 — Credit not refunded on soft-delete of recently-published post LOW

Where: apps/blog-service/src/routes/admin/posts.ts delete handler

The 30-minute grace window is checked only in the unpublish handler. If a tenant hard-soft-deletes a post that was published within the last 30 minutes (without unpublishing first), the credit is not refunded. The tenant loses a credit they'd have recovered if they unpublished first.

Fix: In the delete handler, if status=published and published_at > NOW() - 30min, run the same grace-window refund before setting deleted_at.


Gaps (Missing, Not Just Broken)

Features that were never built. Tracked here for future sprints.

G1 — No cron auto-publish worker

The entire status=scheduledstatus=published transition requires a background trigger that doesn't exist. Scheduling UI is live but non-functional.

What's needed: Cloudflare Cron Trigger + /internal/publish-scheduled handler. See B4.


G2 — Analytics pipeline not wired

METRICS Analytics Engine binding exists in wrangler.toml but is optional and absent from local dev. No ingestion from public reads. get-analytics returns zeros. There is no path from "a post was viewed" to "analytics data exists."

What's needed:

  1. Write an analytics event on every GET /public/posts/:slug response (via waitUntil()).
  2. Build an aggregation query reading from the Analytics Engine dataset.
  3. Wire the result into GET /admin/posts/:id/analytics and the get-analytics MCP tool.
  4. Add METRICS stub to wrangler.toml dev config so local dev doesn't silently skip writes.

G3 — No slug redirect table

Slug changes (from title updates or restore collisions) abandon the old slug with no redirect. Old URLs return 404 permanently.

What's needed:

  1. Migration: blog_post_slug_redirects (tenant_id, old_slug, new_slug, post_id, created_at)
  2. When PUT /admin/posts/:id changes the slug, write an old→new redirect row.
  3. When POST /admin/posts/:id/restore renames the slug, write a redirect row.
  4. On GET /public/posts/:slug 404, check redirect table — if found, return 301 to the new slug.

G4 — MCP tool behavioral divergence is undocumented

publish-post and unpublish-post MCP tools have silent behavioral differences from their REST equivalents. This is undocumented in the tool schema description and will surprise any agent workflow.

What's needed: Update MCP tool descriptions to explicitly state:

  • publish-post: "Note: does not dispatch newsletter. Use the REST endpoint for full publish behavior."
  • unpublish-post: "Note: does not trigger 30-minute grace-window credit refund."

G5 — blog_tags missing timestamps

blog_tags has no created_at or updated_at columns, unlike blog_categories. Can't sort, audit, or paginate by recency.

What's needed: Migration adding created_at TIMESTAMP NOT NULL DEFAULT NOW() to blog_tags.


G6 — No full-text search on posts

Admin list supports category/tag/status filters only. No ?q= search on title, excerpt, or content. Tenants with large post archives have no discovery mechanism beyond scrolling.

What's needed:

  1. Migration: add search_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(excerpt,''))) STORED.
  2. GIN index on search_vector.
  3. Add ?q= param to GET /admin/posts — filter with WHERE search_vector @@ plainto_tsquery('english', ?).

G7 — No image optimization pipeline

featured_image_url is a raw URL stored as-is. No resizing, no CDN rewrite, no WebP conversion. Large originals are served directly to readers.

What's needed: Integration with media-service transform API (if available) or Cloudflare Images to rewrite stored URLs to optimized variants on publish.


G8 — Restore slug collision is silent

When POST /admin/posts/:id/restore detects a slug collision and auto-appends a suffix, the response doesn't call this out explicitly. The caller doesn't know the slug changed.

What's needed: Return { restored: true, slugChanged: true, oldSlug: "...", newSlug: "..." } when a collision is resolved.


Improvement Scope

Priority 1 — Users lose money or subscribers without these

Item Effort Links
Wire cron auto-publish Medium B4, G1
MCP unpublish: add grace-window refund Small B2
MCP publish: add newsletter dispatch (or document skip) Small B3
Credit refund on delete within grace window Small B8
Fix carousel renderer Medium B1

Priority 2 — Correctness and trust

Item Effort Links
Newsletter retry / DLQ Medium B7
Update MCP tool descriptions for behavioral divergence Tiny G4
Fix get-analytics response to be explicit about unavailability Tiny B5

Priority 3 — Missing features

Item Effort Links
Slug redirect table Medium B6, G3
Analytics ingestion pipeline Large G2
Full-text search on admin list Medium G6
blog_tags timestamp migration Tiny G5
Restore slug collision explicit response Tiny G8

Priority 4 — Developer experience

Item Effort Notes
METRICS binding in local dev wrangler config Tiny Currently absent from dev; writes silently skip
Integration tests: publish lifecycle (charge, idempotency, refund) Medium No evidence of coverage on creditGuard edge cases
Integration tests: soft-delete + restore + slug collision Small Restore path has slug collision logic that's untested
Image optimization pipeline Large G7

Fixed Items

Nothing fixed yet — this is the initial backlog from 2026-06-28 analysis.


How to Use This Doc

  • Starting a sprint? Pick items from Priority 1 down.
  • Fixed something? Mark it ✅ with the date and a one-line note on what changed.
  • Found a new issue? Add it to Bugs or Gaps with severity, file location, and a proposed fix.
  • Don't delete fixed items. They serve as a record of what was wrong and when it was resolved.
Blog Engine