# Campaign Studio — Improvement & Growth Plan

> Package: capell-app/campaign-studio · Kind: package · Tier: premium · Product group: Capell Growth · Bundle: growth · Status: Complete

## 1. Snapshot

Campaign Studio is a schema-owning growth package (surfaces `admin` + `frontend`) that adds campaign groups, landing-page variants, UTM attribution, CTA/hero/lead-form widgets, conversion goals, and funnel/overview reporting on top of Capell. It owns five tables (`campaign_groups`, `campaign_landing_pages`, `campaign_cta_widgets`, `campaign_conversion_goals`, `campaign_conversions`) and five models, drives four Filament resources plus three dashboard contributions, and exposes 19 Actions (recording, attribution, variant resolution, URL building, stats, experiment sync/results, public conversion capture, tracker script loading). It hard-requires `admin`, `core`, `form-builder`, `frontend`, `insights`, `layout-builder` and softly supports `experiments` + `seo-suite`/`site-discovery`. Conversion capture flows through the FormBuilder `FormSubmitted` listener (`src/Listeners/RecordFormSubmissionConversion.php`) plus the public Campaign Studio beacon for CTA-click and page-view goals.

Marketplace and Composer copy now use buyer-facing campaign/outcome positioning. Screenshots in `capell.json` marketplace block now include the extension card plus six committed product screenshots for campaign groups, landing-page variants, conversion goals, CTA widgets, dashboard widgets, and the frontend landing page.

## Completed Improvement Slices

- **2026-06-03:** Rewrote marketplace/Composer copy, promoted real product screenshots into the manifest, removed the dead `AttributionModel` enum, and hardened the overview conversion-rate join against blank `utm_campaign` values.
- **2026-06-04:** Added UTM fields to the campaign hero widget configurator and routed hero primary/secondary button URLs through `BuildCampaignUrlAction`, with render coverage proving decorated public URLs do not leak numeric campaign identifiers.
- **2026-06-04:** Added the public campaign conversion beacon, post-load tracker script, typed capture Action/Data boundary, full-page public-output safety tests, and frontend cache contribution metadata proving Campaign Studio output is non-cacheable and varies by UTM targeting dimensions.
- **2026-06-04:** Filtered landing-page variant resolution to linked Capell pages that pass the public `publishedDate()` scope, with Action tests proving expired or scheduled pages are skipped for targeted, primary, and fallback selection.
- **2026-06-04:** Closed the remaining Now reporting/attribution rows by counting distinct Insights visits for duplicate campaign UTMs, adding a configurable conversion attribution lookback window, and exposing typed Campaign Studio experiment result readouts with per-variant conversion rates and lift.
- **2026-06-06:** Added `SyncCampaignStatusesAction`, the `capell:campaign-studio-sync-statuses` command, and an every-five-minutes package schedule so campaign windows transition `Scheduled` to `Active` and `Active` to `Ended`.
- **2026-06-08:** Connected campaign page-view and CTA-click goals into Insights conversion events through `RecordConversionAction`, preserving campaign, goal, URL, value, and source-package metadata while retaining the existing Campaign Studio dedupe policy.

## 2. Improvements (existing functionality)

- **Shipped 2026-06-04: Wire CTA-click conversions to a real capture path.** `POST /capell/campaigns/conversions` now accepts same-origin page-view and CTA-click beacon posts, resolves Insights visits, landing pages, CTA widgets, and conversion goals through `CaptureCampaignConversionAction`, and records conversions through the existing campaign conversion Actions. — restores advertised capability — `src/Actions/CaptureCampaignConversionAction.php`, `src/Http/Controllers/CampaignConversionBeaconController.php`, `src/Support/RenderHooks/RegisterCampaignTrackerHook.php` — L
- **Shipped 2026-06-03: Resolve or remove the `AttributionModel` enum.** `AttributionModel` was removed after review confirmed it had no production consumers and `BuildConversionAttributionAction` records both first- and last-touch fields. — removes misleading API + clarifies attribution semantics — `src/Actions/BuildConversionAttributionAction.php` — M
- **Shipped 2026-06-04: Make `BuildCampaignOverviewStatsAction` conversion-rate join robust.** The overview visit denominator now excludes null/blank campaign UTMs and counts distinct Insights visit ids, so duplicate campaign groups sharing a `utm_campaign` no longer double-count the same visit in the headline conversion-rate KPI. — correctness of a headline KPI — `src/Actions/BuildCampaignOverviewStatsAction.php` — M
- **Shipped 2026-06-04: Variant resolution ignores active/published state of the target page.** `ResolveCampaignLandingPageVariantAction` now filters all targeted, primary, and first-available candidates through the linked page's public `publishedDate()` scope, so scheduled or expired pages are skipped instead of being selected as campaign variants. — prevents serving unpublished content — `src/Actions/ResolveCampaignLandingPageVariantAction.php` — M
- **Eager-load to avoid N+1 in funnel + landing-page queries.** `BuildCampaignConversionFunnelAction` is clean (`withCount`), but `CampaignLandingPagePublicUrlContributor::publicUrls()` loads all landing pages with nested `page.pageUrls.*` and then `unique()`s in PHP — fine for sitemaps but unbounded as campaigns grow. Add chunking/pagination for large sites. — sitemap-build scalability — `src/Support/PublicUrls/CampaignLandingPagePublicUrlContributor.php` — S
- **Shipped 2026-06-04: Hero button URLs bypass `BuildCampaignUrlAction`.** Hero primary/secondary CTAs now use configured widget UTM metadata and `BuildCampaignUrlAction` to append missing UTM parameters while preserving existing query strings and fragments. The current lead-form widget has no button URL; its capture path remains Form Builder submission attribution. — consistent attribution across hero and CTA widget links — `resources/views/components/widget/campaign-hero.blade.php` — S
- **Shipped 2026-06-03: Surface the committed screenshots in the manifest.** The marketplace manifest now references six committed product screenshots in addition to the extension card. — marketplace listing quality — `capell.json` — S
- **Populate the changelog.** `CHANGELOG.md` has a single placeholder line ("Prepared package metadata…"). For a premium first-party package this should track schema/Action changes. — release hygiene / buyer trust — `CHANGELOG.md` — S

## 3. Missing Features (gaps)

Mapped against declared `capabilities[]` (`campaign-audience-targeting`, `campaign-conversion-funnel-reporting`, `landing-page-variant-experiments`, `campaign-experiment-sync`, …) and standard campaign-marketing norms.

- **Shipped 2026-06-04: Client-side conversion capture (table stakes).** Campaign Studio now injects a post-load tracker through the frontend `BodyEnd` render hook and records page-view plus CTA-click conversions through the same-origin Campaign Studio beacon. — **table stakes**.
- **Audience targeting beyond UTM (differentiator vs gap).** `AudienceTargetData` and `ResolveCampaignLandingPageVariantAction` only match `utm_content`/`utm_term`. The capability is named `campaign-audience-targeting`, but there is no geo, device, referrer, returning-vs-new, or time-window targeting — despite `core` depending on `torann/geoip`. Adding GeoIP/device rules (and feeding them into the Experiments audience rules already modelled in `SyncCampaignExperimentAction`) would be a real **differentiator**.
- **Shipped 2026-06-04: A/B test result readout in-package (table stakes for "variant-experiments").** `BuildCampaignExperimentResultsAction` now reads the synced campaign-scoped Experiments winner report back through typed Campaign Studio data, including per-variant conversion rates, winning variant key, and lift over the control variant. — **table stakes**.
- **Shipped 2026-06-06: Scheduling automation.** `SyncCampaignStatusesAction` transitions campaign windows from `Scheduled` to `Active` once `starts_at` opens and from `Active` to `Ended` once `ends_at` closes. The package registers `capell:campaign-studio-sync-statuses` and schedules it every five minutes when installed. — table stakes for "campaign scheduling".
- **Shipped 2026-06-08: Insights conversion loop.** New campaign conversions now also create first-party Insights conversion events named `campaign.{campaign_slug}.{goal_key}` when an Insights visit is present, so Campaign Studio funnels can be reconciled with the broader growth dashboard without adding a public tracking surface. — table stakes for the Growth bundle.
- **Multichannel / channel taxonomy.** Attribution is UTM-only. There is no first-class channel model (email, paid-social, organic) or per-channel rollup in `BuildTopCampaignStudioWidget`. Marketing buyers expect channel breakdowns. — differentiator.
- **Revenue / value attribution.** `value_amount` exists on goals and `budget_amount` on groups, but no Action computes ROAS, cost-per-conversion, or revenue per campaign. The funnel only counts conversions. Surfacing value-weighted conversions would turn reporting from "counts" into "money". — differentiator.
- **Shipped 2026-06-04: Configurable attribution lookback window.** `RecordCampaignConversionAction` now honors `capell-campaign-studio.attribution.lookback_days` (default 30) and drops stale Insights visit identity/UTM attribution outside the window before recording the conversion. — table stakes.
- **Anonymous/no-identity conversion dedup policy.** Conversions with no visit/event/source still skip dedup (`hasIdentity()` returns false → plain `create`). A future row should decide whether anonymous conversions need a bounded dedupe policy by goal, landing page, and time window without collapsing separate anonymous visitors into one conversion. — table stakes.
- **Public landing-page route ownership.** The package contributes URLs to Site Discovery but renders through `core`'s `capell.pages.show`. There is no campaign-native short URL / vanity slug (`/go/{campaign}`) that auto-applies UTMs and redirects — a common campaign-tool feature. — differentiator.

## 4. Issues / Risks

- **Shipped 2026-06-04: Cache-vs-personalization contradiction.** Campaign Studio now records a non-cacheable frontend render contribution with UTM variance metadata whenever its tracker is injected, and docs clarify that conversion capture is post-load/cache-compatible while UTM-targeted variant selection remains dynamic frontend output. Tests assert the tracker contribution is non-cacheable and carries `utm_campaign`, `utm_content`, and `utm_term`. — `capell.json` (performance block), `src/Support/RenderHooks/RegisterCampaignTrackerHook.php` — **High**.
- **Shipped 2026-06-03: Dead enum shipped in a premium API.** `AttributionModel` was removed after review confirmed it had no production consumers and `BuildConversionAttributionAction` always records both first- and last-touch fields. — Medium.
- **Shipped 2026-06-04: CTA/page-view Actions are untested _in integration_ and unreachable in production.** Feature tests now cover route registration, page-view capture, CTA-click capture, invalid-origin rejection, and deduping through Insights visit identity/source context. — Medium.
- **Shipped 2026-06-04: No frontend render/feature test.** Feature coverage now renders a full public campaign page fragment with widget output plus the tracker hook and asserts no authoring markers, model fields, numeric campaign id attributes, or signed editor URLs leak. — Medium.
- **Public-output safety: currently OK, keep it that way.** `tracking/attributes.blade.php` and `campaign-cta-widget.blade.php` expose only `campaignGroup->slug` and goal/cta `key` — no numeric IDs, field paths, or model names — and a test guards this. The hero/lead-form widgets render `data-campaign-goal` from `getMeta('goal_key')` (admin-authored string), which is acceptable. Risk is regression if a future change emits IDs; lock with a render-level (not just component-level) assertion. — `resources/views/components/**` — Low (but easy to regress).
- **`SyncCampaignExperimentAction` uses `forceFill(...)->save()` and direct relation writes.** It bypasses the Experiments package's own create Action on the update path (only the create path calls `CreateExperimentAction`). If Experiments adds validation/events to variant/goal writes, this drifts. Cross-package write coupling is a maintenance risk. — `src/Actions/SyncCampaignExperimentAction.php` L48-65, L199-250 — Medium.
- **Shipped 2026-06-04: `utm_campaign` join double-counted duplicate campaigns.** Multiple campaign groups can still share a `utm_campaign` (only `slug` is unique), but the overview/visit join now counts distinct Insights visits so duplicate groups do not inflate the denominator. Shared UTM values remain analytically ambiguous and should be avoided for precise campaign attribution. — `src/Actions/BuildCampaignOverviewStatsAction.php` — Medium.
- **i18n completeness.** Enum labels and Filament strings are translated via `__()` (good). Verify all five lang files (`form`, `generic`, `navigation`, `package`, `widgets`) carry every key the code references (e.g. `generic.landing_page_variant`, `widgets.active_campaign-studio`); the odd `active_campaign-studio` array key (hyphen in an array key/translation slug) is fragile. — `resources/lang/en/*`, `src/Actions/BuildCampaignOverviewStatsAction.php` — Low.
- **Performance budget unverified.** `adminQueryBudget: 40` and `frontendRenderBudgetMs: 20` are declared but no test or benchmark asserts them; the CTA hydrate batch (`CampaignCtaWidget::hydrateWidgets`) is well-designed for it, but unmeasured. — `capell.json` — Low.

## 5. Marketplace & Selling

The manifest and Composer description now use this buyer-facing one-sentence summary:

> Launch, target, and measure marketing campaigns inside Capell — build landing-page variants, drop in CTA and lead-capture widgets, and track UTM-attributed conversions and funnels without bolting on a separate analytics tool.

The package should continue toward this fuller buyer-facing product story as the remaining conversion-capture gaps land:

> Campaign Studio turns Capell into a campaign command centre for marketing and growth teams. Group your landing pages, CTAs, and lead forms under a campaign, target visitors with UTM-driven page variants, and let conversions flow in automatically from form submissions and on-page goals. Built-in funnel and overview reporting show which campaigns and pages actually convert — and how that rate trends week over week — without exporting to a third-party tool. When the Experiments add-on is installed, your landing-page variants and conversion goals sync straight into a running A/B test.

**Screenshot / media gaps.** Manifest now exposes the extension card plus six real product screenshots. Missing entirely: a hero showing the funnel/overview stats, and an animated GIF of variant selection or the experiment sync. Dark captures exist in `docs/screenshots/` but are not yet promoted into marketplace media; decide whether to add them or keep the listing focused on the light captures.

**Pricing / tier / bundle positioning.** Premium tier in the `growth` bundle is right — this is a paid, first-party, priority-support package. It is the natural _anchor_ of the Capell Growth bundle: it hard-requires `insights` (analytics), `form-builder` (lead capture), and `layout-builder` (widgets), so selling Campaign Studio pulls those in. Cross-sell levers: (1) `experiments` add-on as an upsell unlocked by the existing `SyncCampaignExperimentAction`; (2) `seo-suite` / `site-discovery` for sitemap + AI-discovery of landing pages (already wired via `CampaignLandingPagePublicUrlContributor`); (3) bundle with a future "Automation Studio" that listens to the already-dispatched `CampaignConverted` event. Position as the Extension Suite centrepiece: "Campaign Studio + Experiments + SEO Suite = the Capell Growth Suite."

**Differentiators / value props / target buyer.** Target buyer: the in-house marketer or agency running campaigns on a Capell-built site who today stitches together GA + a form tool + spreadsheets. Value props: campaigns, variants, widgets, and conversion reporting native to the CMS (no tag-manager glue); UTM attribution captured server-side and deduped; one-click sync to real A/B experiments; landing pages that automatically enter the sitemap and AI-discovery feeds. Differentiator to lean into: _closed-loop_ — the same campaign that builds the page also reports its conversions and feeds the experiment.

**Keywords / tags (8–12):** campaign management, landing pages, conversion tracking, UTM attribution, A/B testing, lead capture, marketing CMS, CTA widgets, conversion funnel, growth marketing, audience targeting, Filament.

## Completion Review

Completed 2026-06-08. The current manifest-backed plan is closed: Campaign Studio ships campaign/landing-page/goal admin surfaces, safe UTM-decorated widgets, a public beacon for page-view and CTA-click conversions, scheduled campaign status automation, experiment result readouts, public-output/cache-safety tests, and a first-party Insights conversion bridge. Revenue/ROAS, deeper audience targeting, anonymous dedupe policy, campaign vanity URLs, and looser Experiments write coupling remain future product-depth candidates rather than active completion blockers.

## 6. Prioritized Roadmap

| Item                                                                                                                            | Bucket | Effort | Impact | Section ref |
| ------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ------ | ----------- |
| Shipped 2026-06-04: Wire CTA-click + page-view conversion capture (beacon)                                                      | Done   | L      | High   | §2, §3      |
| Shipped 2026-06-04: Add frontend render/feature tests proving widgets render + no ID/marker leak in full page                   | Done   | M      | High   | §4          |
| Shipped 2026-06-04: Resolve the cacheable=false vs static-HTML-cache personalization contradiction (tracker contribution + doc) | Done   | M      | High   | §4          |
| Shipped 2026-06-04: Filter variant resolution to published pages                                                                | Done   | M      | Medium | §2          |
| Shipped 2026-06-04: Harden overview conversion-rate join (null/duplicate `utm_campaign`)                                        | Done   | M      | Medium | §2, §4      |
| Shipped 2026-06-06: Add campaign scheduling command (Scheduled→Active→Ended transitions)                                        | Done   | M      | Medium | §3          |
| Shipped 2026-06-04: In-package A/B variant results readout (lift per variant)                                                   | Done   | L      | High   | §3          |
| Shipped 2026-06-08: Feed Campaign Studio conversions into Insights conversion events                                            | Done   | M      | High   | §3, §5      |
| Revenue/ROAS reporting using existing `value_amount` + `budget_amount`                                                          | Future | M      | High   | §3          |
| Geo/device/referrer audience targeting (use `torann/geoip`)                                                                     | Future | L      | High   | §3          |
| Shipped 2026-06-04: Configurable attribution lookback window                                                                    | Done   | M      | Medium | §3, §4      |
| Anonymous/no-identity conversion dedup policy                                                                                   | Future | M      | Medium | §3, §4      |
| Decouple `SyncCampaignExperimentAction` update path from direct relation writes                                                 | Future | M      | Low    | §4          |
| Populate CHANGELOG + benchmark perf budgets (20ms render / 40 query)                                                            | Future | S      | Low    | §2, §4      |