Skip to content

Publishing Studio — Improvement & Growth Plan

Package: capell-app/publishing-studio · Kind: package · Tier: premium · Product group: Capell Publishing Pro · Bundle: publishing-pro · Status: Complete

Publishing Studio is Capell’s flagship editorial-workflow package: copy-on-write workspaces (drafts) over any registered Draftable model, atomic versioned publish, rebase/stale-detection, rollback + entity restore, signed live preview links, an approval chain, a durable scheduler (publish / unpublish / embargo / review-reminder events), an editorial calendar that aggregates contributors (Blog, Campaign Studio, Newsletter, Events), and four admin pages (Workflow command centre, Activity trail, Scheduled publishing, Stale drafts) plus two Filament resources (WorkspaceResource, PreviewLinkResource) and several dashboard widgets/Livewire panels. It is large and mature: 242 src/ PHP files, 132 test files, surfaces ["admin","console"]. Core engine lives in root-namespaced classes (Publisher, Rebaser, Rollback, ReleaseWindowGuard) plus ~45 Actions; data models are Workspace, Version, PublishingRevision, PreviewLink, SchedulerEvent, SchedulerDelivery, SchedulerIcalToken, WorkspaceApproval, WorkspaceFieldComment, WorkspaceReviewAssignment over 12 migrations (workspaces, versions, preview_links, publishing_revisions, publishing_scheduler_events, approvals/comments/assignments, plus workspace_id/shadowed_by_workspace_id columns added to core + external tables). Dependencies: capell-app/{admin,core,html-cache,migration-assistant,navigation} + jfcherng/php-diff. Marketplace summary now leads with the review-first CMS value proposition, and the manifest declares the extension card plus populated editor/reviewer workflow captures. Empty workflow, preview-link, scheduled-publishing, and stale-draft captures were demoted until seeded recapture.

Prioritized.

  • Closed 2026-06-05: Co-locate publish-check + release-window config defaults inside the package. — Shipped config/publishing-studio.php, merged it as capell.publishing-studio, published it for console installs, and included default publish checks, release-window settings, scheduled publishing, prune schedule, and preview home-route config. — config/publishing-studio.php, src/Providers/PublishingStudioServiceProvider.php — M
  • Done/Shipped: Make the manifest health check actually verify something. PublishingStudioHealthCheck now reports real diagnostics for required workflow/revision/scheduler tables and configured publish-readiness checks, with focused health coverage. — src/Health/PublishingStudioHealthCheck.php, tests/Feature/Health/PublishingStudioHealthCheckTest.php — M
  • Closed 2026-06-05: Fix the load-testing capability so it works on a real install. LoadTestPublishingStudioCommand now resolves a package-owned fixture model from src/Support/LoadTesting, so the advertised command no longer depends on tests/ autoload. — src/Console/Commands/LoadTestPublishingStudioCommand.php, src/Support/LoadTesting/WorkspaceDraftableFixture.php — S
  • Done/Shipped: broken-links is clarified as internal link integrity. The legacy identifier remains broken-links for config compatibility, but the surfaced label and messages now say “Internal link integrity” and explicitly validate only internal Capell URLs against page_urls; external URLs are ignored until a future opt-in HTTP probe exists. — src/Checks/BrokenLinkCheck.php, resources/lang/en/workspace.php, tests/Integration/Checks/IndividualChecksTest.php — M
  • Done/Shipped: legacy scheduled-publish job is a durable scheduler shim. PublishScheduledPublishingStudioJob no longer runs a separate Workspace scan-and-publish loop. It delegates to RunDueSchedulerEventsAction::run(includePublish: true), so publish, unpublish, and reminder execution all flow through claimed SchedulerEvent rows and ExecuteSchedulerEventAction. — src/PublishScheduledPublishingStudioJob.php, src/SchedulePublishAction.php, tests/Integration/PublishScheduledPublishingStudioJobTest.php — M
  • Done/Shipped: central publish-readiness data boundary. BuildPublishReadinessAction returns PublishReadinessData from the dry-run report, URL collisions, stale conflicts, and publish-check failures. ValidateAction now consumes that typed boundary instead of interpreting DryRunReport directly, and dry runs can respect release-window bypasses for authorized callers. — src/Actions/BuildPublishReadinessAction.php, src/Data/PublishReadinessData.php, src/Publisher.php, src/Filament/Resources/PublishingStudio/Actions/ValidateAction.php, tests/Integration/PublisherDryRunTest.php — M
  • Closed 2026-06-05: Populate the CHANGELOG. The changelog now records marketplace value-copy/media expansion, real health diagnostics, manifest settings/table reconciliation, and the production-autoloadable load-test fixture. — CHANGELOG.md — S

Tied to capabilities[] and editorial-workflow norms.

  • Field-level merge / three-way diff resolution (differentiator). Rebaser surfaces conflicting uuids and supports only coarse keep-mine / take-live per record; there is no field-level merge when two editors touch different fields of the same record. Statamic/Sanity-class tools resolve at field granularity. This is the single biggest differentiator available given the diff infrastructure (WorkspaceDiffService, jfcherng/php-diff) already exists. — src/Rebaser.php, src/Services/WorkspaceDiffService.php
  • Scheduled-content calendar drag-to-reschedule (table stakes for “editorial calendar”). The calendar (ContentSchedulerCalendarWidget, BuildEditorialCalendarEventsAction) aggregates and displays events but reschedule still goes through SchedulerMetadataAction forms. Inline drag-reschedule is expected of an editorial calendar.
  • Approval workflow configurability beyond numeric levels. Workspace::approve() uses settings.requiredApprovalLevels (default 2) — purely a count. Norms include named stages, per-role required approvers, and per-content-type policies. ReviewPolicyResolver/RequiredReviewer exist but the public surface is a bare integer. — src/Models/Workspace.php, src/Approvals/ReviewPolicyResolver.php
  • Multi-site / cross-site coordinated publish. Release Workspaces group content for one atomic publish, but siteId() derivation in SyncWorkspaceSchedulerEventsAction picks the first page’s site; there is no first-class “publish this release to sites A+B” concept. Given Capell is multi-site, this is a notable gap. — src/Actions/SyncWorkspaceSchedulerEventsAction.php
  • Notifications breadth. Only WorkspaceStateNotification and WorkspaceReviewReminderNotification exist. Editorial teams expect Slack/Teams/webhook fan-out and digest summaries (“3 drafts awaiting your approval”). Could cross-sell into a notifications/automation package.
  • Embargo on the public read path. embargo_until blocks publish, but there is no evidence of a public-render guard that hides embargoed-but-scheduled content if a stale cache or preview leaks it. Worth an explicit anonymous-safety test asserting embargoed content never renders to guests.
  • Diff/restore for media and layout blocks. MediaDiffService exists, but entity restore (Rollback/EntityRollbackAction) is row-oriented; granular per-asset restore and layout-block-level rollback would round out entity-restore.
  • Closed 2026-06-05: Publish checks may be silently no-op (highest risk). Default config now resolves to the shipped publish checks, and PublishCheckPipelineTest asserts the configured defaults are non-empty, implement PublishCheck, resolve through the container, and return pipeline results. — src/Checks/PublishCheckPipeline.php, tests/Integration/PublishCheckPipelineTest.php
  • Closed 2026-06-05: Manifest ↔ code drift. capell.json now declares PublishingStudioSettings, database.settings: true, the package-owned storage table set, the extension card, desktop/mobile hero images, and runner-backed workflow captures. load-testing now resolves a production-autoloadable fixture model. — capell.json, src/Settings/PublishingStudioSettings.php, docs/screenshots.json, src/Support/LoadTesting/WorkspaceDraftableFixture.php
  • Done/Shipped: admin query budget is asserted for dashboard and calendar Actions. AdminQueryBudgetTest reads performance.adminQueryBudget from capell.json and profiles the workflow command center, site stats, workspace activity, merge history, scheduler event builder, and editorial calendar builder against the 40-query ceiling. — capell.json, tests/Feature/Performance/AdminQueryBudgetTest.php
  • frontendRenderBudgetMs: 0 vs a real frontend surface. The package does serve frontend HTTP (signed preview render path through ResolveWorkspaceContext, plus the exit-preview route), yet surfaces omits "frontend" and the render budget is 0. Preview rendering disables cache (config(['capell-core.disable_cache' => true])) so it is inherently slower than a cached page; document/measure the preview render path rather than declaring 0. — capell.json, src/Http/Middleware/ResolveWorkspaceContext.php
  • Concurrency: strong, keep it tested. Publisher row-locks the live Version and re-checks for a shifted live id (throws StaleWorkspaceException); ExecuteSchedulerEventAction claims events via lockForUpdate + a 15-minute stale-claim reclaim; SQLite path is documented as serialised. PublisherConcurrencyTest, PublisherEmbargoTest, PublisherReleaseWindowTest, PublishScheduledPublishingStudioJobTest cover this. Risk is low — preserve these tests on any scheduler refactor.
  • Closed 2026-06-05: Public-output safety explicit coverage. Preview is signed (URL::temporarySignedRoute) with a DB-backed revocable token as the authoritative key, signed cms_workspace cookie (hash_equals), userMayResolve() re-checks view policy so revoked users lose context, and workspace responses set Cache-Control: private, no-store. The live frontend test now proves an anonymous request to the live URL keeps rendering published copy while hiding embargoed draft copy, workspace names, preview markers, workspace cookies, and preview-only private/no-store headers. — src/Http/Middleware/ResolveWorkspaceContext.php, src/Actions/GenerateWorkspacePreviewUrlAction.php, src/Http/Controllers/SchedulerIcalFeedController.php, tests/Feature/Page/PageWorkspaceDraftTest.php
  • Scheduler siteId heuristic. SyncWorkspaceSchedulerEventsAction::siteId() takes the first workspace-scoped page’s site_id; for a multi-site release workspace this is ambiguous and can misattribute calendar/feed scoping. — src/Actions/SyncWorkspaceSchedulerEventsAction.php
  • i18n. Labels are translation-key driven (capell-publishing-studio::*) — good. Confirm scheduler/notification human strings and exception messages (several are hard-coded English in Publisher/Rebaser, e.g. “Workspace #%d must be approved…”) are acceptable as developer-facing only, not user-facing. — src/Publisher.php, src/Rebaser.php
  • Test-coverage gaps. Config-default publish-check resolution, embargo/live-path safety, and LoadTestPublishingStudioCommand production-style autoload coverage are now present. Multi-site release scoping remains the notable missing dedicated test. Coverage is otherwise broad.

Critique. The current summary and composer description are accurate but read as a feature list, not a value proposition — they enumerate verbs (preview/compare/approve/schedule/publish/restore/rollback) without naming the pain (publishing directly to live, no review trail, no safe rollback). The composer description (“Editorial workflow package for Capell revisions, scheduling, approvals, and controlled publishing”) is serviceable but generic.

Improved 1-sentence summary.

Publishing Studio turns Capell into a review-first CMS: edit any content in an isolated draft workspace, get sign-off, schedule it, and publish atomically — with one-click rollback when something breaks.

Improved 3–4 sentence description.

Publishing Studio is Capell’s premium editorial workflow. Every change happens in a copy-on-write workspace that stays invisible to visitors until it’s approved and published as a single atomic, versioned release — so the live site is never half-edited. Built-in publish-readiness checks (accessibility, SEO meta, alt text, internal links, URL collisions, stale-draft and release-window guards) stop risky publishes before they ship, while signed, revocable preview links let stakeholders review the exact pending state. When a publish goes wrong, roll back to any prior version or restore individual entities in seconds.

Screenshot / media status. The manifest now exposes the extension card plus the populated editor/reviewer workflow captures. Empty workflow dashboard, preview-link, scheduled-publishing, and stale-draft captures were demoted. The contract still asks for additional route/admin captures for live preview, compare/readiness, approval history, scheduler metadata, activity history, rollback/restore, and preview banner states; a 20-30s capture of the draft-to-approve-to-schedule-to-publish-to-rollback loop would still strengthen the listing.

Pricing / tier / bundle positioning. Correctly premium in the publishing-pro bundle — this is anchor-tenant functionality, not an add-on. Position it as the headline of the Publishing Pro bundle. Natural cross-sell follows the dependency + supports graph: migration-assistant (recovery/import into workspaces), html-cache (publish-time invalidation), and the soft contributors — blog, campaign-studio, newsletter, events — which feed the editorial calendar. Bundle messaging: “buy Publishing Pro, get a governed editorial calendar across every content type you publish.”

Differentiators / value props / target buyer. Differentiators: atomic multi-model release workspaces, durable scheduler with embargo + release windows, signed revocable previews, and true version rollback + entity restore — capabilities usually reserved for Statamic/Sanity/Contentful-tier products. Target buyer: agencies and in-house teams running multi-author, compliance-sensitive, or brand-controlled sites where publishing directly to production is unacceptable.

Keywords/tags (8–12). editorial-workflow, content-approval, draft-publishing, scheduled-publishing, content-versioning, rollback, live-preview, release-windows, publish-readiness, editorial-calendar, content-governance, workspaces.

ItemBucketEffortImpactSection ref
Ship package config with publish-check + release-window defaults (mergeConfigFrom)DoneMHigh§2, §4
Test asserting default config resolves ≥1 publish check (guard against silent no-op)DoneSHigh§4
Done/Shipped: Reconcile manifest with code: declares settings, package-owned storage tables, expanded screenshots[], and production-autoloadable load-testing fixtureDoneSHigh§4
Done/Shipped: Anonymous-safety test proves guests never see workspace markers or embargoed content on live pathDoneSHigh§4
Done/Shipped: Populate CHANGELOG with real release historyDoneSMed§2
Done/Shipped: Give the manifest health check real assertions (tables and checks resolvable)DoneMMed§2
Done/Shipped: Fix load-test fixture to a production-autoloadable modelDoneSMed§2, §4
Consolidate legacy publish job onto durable scheduler eventsDoneMMed§2 — closed 2026-06-08: PublishScheduledPublishingStudioJob delegates to RunDueSchedulerEventsAction::run(includePublish: true), so scheduled publishing now uses durable claimed scheduler events.
Central BuildPublishReadinessAction returning typed DataDoneMMed§2 — closed 2026-06-08: BuildPublishReadinessAction returns PublishReadinessData, and ValidateAction consumes it for failure/warning/success notifications.
Clarify/extend broken-links (rename or add opt-in external probe)DoneMMed§2, §3 — closed 2026-06-08: surfaced copy now says “Internal link integrity”; the legacy broken-links identifier remains for config compatibility, and external URLs are explicitly ignored.
Profile dashboard/calendar Actions against adminQueryBudget: 40; assert query ceiling in testsDoneMMed§4 — closed 2026-06-08: AdminQueryBudgetTest profiles workflow, dashboard, scheduler, and editorial-calendar Actions against the manifest budget.
Recapture populated Publishing Studio workflow, preview-link, scheduled-publishing, and stale-draft screenshots before promoting them as product mediaLaterSMed§5 — media-only follow-up; implementation scope is complete and promoted screenshots should wait for populated runner captures.
Field-level three-way merge in Rebaser (flagship differentiator)LaterLHigh§3
Configurable named approval stages / per-role required approversLaterLMed§3
First-class multi-site coordinated release publish + correct calendar site scopingLaterLMed§3, §4
Calendar drag-to-reschedule + Slack/webhook + digest notificationsLaterLMed§3
Granular media/layout-block restoreLaterLLow§3