2 · Engagement (9B)
Objective — keep new users reaching value and existing users informed by wiring four engagement streams: onboarding, a public changelog, a feedback channel, and an optional blog.
Steps at a glance:
- Choose and wire the engagement streams — Decide which of the four streams to build now, pick a tool for each, and wire them in. Onboarding, changelog, and feedback together form the gate; the blog is optional but strong for organic growth.
- Add first-run onboarding — Wire a welcome modal that fires on the user’s first login, then a persistent checklist widget so they can track early tasks.
- Install a feedback widget — Embed the ProductLift widget so users can submit feature requests and bug reports directly from the app, with their identity pre-filled.
- Publish a changelog — Enable ProductLift’s Changelog module and create an initial “Welcome” entry so users can track what’s shipped.
Background
Section titled “Background”Engagement is the difference between users who reach value once and users who stay. Each stream below targets a different moment in the lifecycle — first run, “what’s new,” and the feedback loop.
Use Zaj-PROJECT.md to record which engagement streams are active, their provider/tool choices, non-secret board/widget IDs, public changelog URL, and whether the blog is adopted or intentionally N/A. Product copy and detailed implementation notes can live in the files created below.
flowchart LR OB[Onboarding] --> VAL[User reaches value] CL[Changelog] --> TRUST[Trust + retention] FB[Feedback] --> LOOP[Product loop] BL[Blog optional] --> SEO[SEO + announcements] VAL --> CL TRUST --> FB1. Choose and wire the engagement streams
Section titled “1. Choose and wire the engagement streams”Decide which of the four streams to build now, pick a tool for each, and wire them in. Onboarding, changelog, and feedback together form the gate; the blog is optional but strong for organic growth.
-
Wire each engagement stream from the table below, then record what you built.
Stream What Note Onboarding First-run guidance so new users reach value fast Reduces churn at the riskiest moment Changelog A public record of what shipped Builds trust; pairs with Phase 8 changelog item Feedback A collection channel (widget/form) Closes the loop with real users Blog Content surface for SEO + announcements Optional; strong for organic growth - ✅ An onboarding flow, a public changelog, and a feedback channel are live (blog optional).
-
Optional — stand up the Blog stream. First check for a built-in blog before adding a static site:
Terminal window grep -r "blog" config/ --include="*.php" | head -5php artisan route:list | grep -i blogIf routes exist and the editor is sufficient, write posts through the admin panel. If not, run a separate static blog (Astro or Hugo) at
/blogfor best SEO, orblog.<DOMAIN>for easiest deployment.Option Best for AstroWind A polished marketing site plus blog AstroPaper A simple writer-first Astro blog Hugo-PaperMod A fast, low-JS blog with long-term maintenance simplicity Terminal window command -v npm >/dev/null || { echo "install Node/npm first"; exit 1; }npm create astro@latest your-blog -- --template onwidget/astrowind# astro.config.mjs: site: 'https://YOUR_DOMAIN', base: '/blog'npm run build && npm run previewHugo + PaperMod is the lean alternative when you want a fast static blog with fewer JavaScript moving parts:
Terminal window command -v hugo >/dev/null || { echo "install hugo first (brew install hugo)"; exit 1; }hugo new site your-blogcd your-bloggit initgit submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperModprintf 'theme = "PaperMod"\nbaseURL = "https://YOUR_DOMAIN/blog/"\n' >> hugo.tomlhugo server- ✅ The blog, if chosen, is reachable and passes a mobile PageSpeed check.
2. Add first-run onboarding
Section titled “2. Add first-run onboarding”Wire a welcome modal that fires on the user’s first login, then a persistent checklist widget so they can track early tasks.
Pick onboarding type by product complexity:
| Type | Best for |
|---|---|
| Welcome modal | Simple products, quick start |
| Guided tour / tooltips | Complex products, feature discovery |
| Onboarding checklist | SaaS with setup steps |
| Email drip | Complement to in-app onboarding |
Define 5–7 essential first steps that get a new user to core value: complete profile, create first project/item, invite a team member, connect an integration, explore dashboard. Use actions the app actually has; do not invent routes or columns.
Rollout: MVP = welcome modal + checklist + welcome email; Phase 2 = guided tour + contextual tooltips; Phase 3 = segmentation + A/B testing + email drip. The steps below scaffold the MVP.
-
Create the migration — add a
has_seen_welcomeboolean column to the vendor’suserstable.Terminal window php artisan make:migration add_has_seen_welcome_to_users_table_zajIn the generated migration — guarded both ways:
public function up(): void{Schema::table('users', function (Blueprint $table) {if (! Schema::hasColumn('users', 'has_seen_welcome')) {$table->boolean('has_seen_welcome')->default(false)->comment('ZAJ: onboarding welcome-modal seen flag');}});}public function down(): void{Schema::table('users', function (Blueprint $table) {if (Schema::hasColumn('users', 'has_seen_welcome')) {$table->dropColumn('has_seen_welcome');}});}Apply it, then confirm the column landed:
Terminal window php artisan migratephp artisan tinker --execute="echo \Schema::hasColumn('users','has_seen_welcome') ? 'ok' : 'MISSING';"# Expected: ok- ✅ Column added under a
Schema::hasColumn()guard, with a reversibledown();migrate:freshwas not used.
- ✅ Column added under a
-
Add the route — in
routes/web.php:Route::post('/onboarding/mark-seen', function () {Auth::user()->update(['has_seen_welcome' => true]);return response()->json(['status' => 'ok']);})->middleware('auth');Confirm it registered:
Terminal window php artisan route:list --path=onboarding/mark-seen# Expected: one POST row for onboarding/mark-seen with the auth middleware- ✅ The route is registered and auth-gated.
-
Create the welcome modal —
resources/views/components/onboarding/welcome-modal.blade.php:<div x-data="{ open: {{ auth()->user()->has_seen_welcome ? 'false' : 'true' }} }"x-show="open"class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"><div class="bg-white rounded-xl p-8 max-w-md w-full shadow-xl"><h2 class="text-2xl font-bold mb-2">Welcome to {{ config('app.name') }}!</h2><p class="text-gray-600 mb-6">Here's how to get started…</p><button @click="open = false; fetch('/onboarding/mark-seen', { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content, 'Accept': 'application/json' }})"class="btn-primary w-full">Get started</button></div></div>Include it in
resources/views/layouts/app.blade.php(after<body>):@auth<x-onboarding.welcome-modal />@endauth -
Create the checklist widget —
resources/views/components/onboarding/checklist.blade.php:Pick completion signals your app actually has — do not invent columns. Examples: profile photo path, a
projects()relation, or a nullable timestamp you add in the same migration batch:// Optional second column in the same migration (only if you want a profile gate).// Same guard discipline — it also extends the vendor's users table:if (! Schema::hasColumn('users', 'profile_completed_at')) {$table->timestamp('profile_completed_at')->nullable()->comment('ZAJ: onboarding profile-completion gate');}@php$user = auth()->user();$steps = [['label' => 'Complete your profile', 'url' => '/profile', 'completed' => filled($user->profile_photo_path ?? $user->avatar ?? null) || filled($user->profile_completed_at ?? null)],['label' => 'Set up your first project', 'url' => '/projects/create', 'completed' => method_exists($user, 'projects') && $user->projects()->exists()],];@endphp<div class="bg-white border rounded-lg p-4 shadow-sm"><h3 class="font-semibold mb-3">Getting started</h3>@foreach ($steps as $step)<div class="flex items-center gap-2 py-1">@if ($step['completed'])<span class="text-green-500">✓</span>@else<span class="text-gray-300">○</span>@endif<a href="{{ $step['url'] }}" class="text-sm {{ $step['completed'] ? 'line-through text-gray-400' : '' }}">{{ $step['label'] }}</a></div>@endforeach</div> -
Optional — add a guided tour with driver.js that fires only on the user’s first login.
Terminal window npm install driver.jsimport { driver } from "driver.js";import "driver.js/dist/driver.css";const driverObj = driver({showProgress: true,steps: [{ element: '#create-btn', popover: { title: 'Create Project', description: 'Start here.' } },{ element: '#settings-link', popover: { title: 'Settings', description: 'Configure preferences.' } },],});if (document.body.dataset.firstLogin === 'true') driverObj.drive();Set
data-first-login="true"on<body>from the samehas_seen_welcomesignal. Match selectors to real elements — do not invent IDs. -
Optional — use Produktly instead of hand-rolled onboarding. Pick this or the modal/checklist/driver.js path, not both. Wire the loader via config and consent gate; never inline the token.
PRODUKTLY_CLIENT_TOKEN=config/services.php 'produktly' => ['client_token' => env('PRODUKTLY_CLIENT_TOKEN')],@if (config('services.produktly.client_token') && (function_exists('consent') ? consent('analytics') : request()->cookie('consent_analytics') === 'yes'))<script>// Load Produktly after consent; identify with opaque IDs only.</script>@endifSecurity: send opaque IDs only, enable HMAC user verification, add
https://public.produktly.comto CSP, and disclose the processor in the privacy policy.
- Expected: Create a fresh user → log in → welcome modal appears → dismiss it → checklist widget is visible in the UI → complete a checklist task → its row shows a tick → log out and back in → modal does not reappear.
3. Install a feedback widget
Section titled “3. Install a feedback widget”Embed the ProductLift widget so users can submit feature requests and bug reports directly from the app, with their identity pre-filled. Do this before the changelog nav link — the widget script registers the data-productlift-changelog handler.
-
Create a Feature Requests board in ProductLift — 👤 human signs in to ProductLift → Boards → New board → name it “Feature Requests” → customise branding to match your app.
-
Install the widget snippet — add the following before
</body>inresources/views/layouts/app.blade.php:{{-- ProductLift Widget — ONLY after the user accepted functional/analyticsconsent (Phase 6 §6 gate). It ships id/email/name to a third party, so itmust not load for a visitor who hasn't consented. Match the condition toyour consent helper/cookie from Phase 6. --}}@if (function_exists('consent') ? consent('analytics') : request()->cookie('consent_analytics') === 'yes')<script>window.ProductLift = {id: @json(config('services.productlift.id')),user: {id: @json((string) Auth::id()),email: @json(Auth::user()->email ?? ''),name: @json(Auth::user()->name ?? '')}};</script><script src="https://productlift.dev/widgets/v1/widget.js" async></script>@endifSet the widget ID via env (
PRODUCTLIFT_ID) →config/services.php('productlift' => ['id' => env('PRODUCTLIFT_ID')]) — never commit a literal ID inline. The@ifgate must mirror whatever consent signal your Phase-6 banner writes. -
Commit:
Terminal window git add resources/views/layouts/app.blade.phpgit commit -m "feat(feedback): install ProductLift widget with identity pre-fill"
- Expected: Human app sign-in exists → feedback widget button is visible → agent clicks it and submits a non-secret test feature request → human opens/signs in to the ProductLift dashboard if needed → the request appears with the correct user name and email.
4. Publish a changelog
Section titled “4. Publish a changelog”Enable ProductLift’s Changelog module and create an initial “Welcome” entry so users can track what’s shipped.
-
Enable the Changelog module in ProductLift — 👤 human signs in to the ProductLift dashboard → Settings → Modules → toggle Changelog on.
The feedback widget from section 3 above loads the script that powers the changelog popup — no extra snippet is needed.
-
Create your first entry — click Changelog → New post. Title: “Welcome to [App Name] — here’s what’s new.” Publish it.
-
Add a “What’s New” trigger in your app layout — in
resources/views/layouts/app.blade.php, add a link or bell icon that opens the ProductLift changelog popup:<a href="#" data-productlift-changelog class="nav-link">What's New</a>(ProductLift’s widget script registers the
data-productlift-changeloghandler automatically.)
- Expected: “What’s New” link/icon is visible in the app nav → clicking it opens the ProductLift changelog panel → the welcome entry appears → features marked “Shipped” in ProductLift are linkable from within the panel.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 👤 Onboarding — first-run guidance so new users reach value fast.
- 🤖 Onboarding — modal + migration —
has_seen_welcomecolumn migrated; modal fires on first login and is dismissed without reappearing. - 🤖 Onboarding — checklist widget — checklist widget visible after modal dismissed; completed tasks show a tick.
- 🔀 Changelog — a public record of what shipped (pairs with the Phase 8 changelog item).
- 🤖 Changelog — “What’s New” link — link/icon in app nav opens the ProductLift changelog panel.
- 🔀 Feedback — a collection channel (widget/form) is reachable.
- 🤖 Feedback — widget installed — ProductLift snippet in layout; test submission appears in dashboard with correct user identity.
- 👤 Blog (optional) — content surface for SEO + announcements, if chosen.
- 🤖 Project state updated —
Zaj-PROJECT.mdrecords the active engagement streams, provider/tool choices, non-secret board/widget IDs, changelog URL, and blog adoption orN/Aevidence.