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.
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.
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 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 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:Terminal window php artisan make:migration add_has_seen_welcome_to_users_tableIn the generated migration:
$table->boolean('has_seen_welcome')->default(false);Terminal window php artisan migrate -
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'); -
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):$table->timestamp('profile_completed_at')->nullable();@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 — log in → 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: Log in → feedback widget button is visible → click it → submit a test feature request → open the ProductLift dashboard → 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 — log in to your 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.