Skip to content
prod e051e98
Browse

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.

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 --> FB

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.

  1. Wire each engagement stream from the table below, then record what you built.

    StreamWhatNote
    OnboardingFirst-run guidance so new users reach value fastReduces churn at the riskiest moment
    ChangelogA public record of what shippedBuilds trust; pairs with Phase 8 changelog item
    FeedbackA collection channel (widget/form)Closes the loop with real users
    BlogContent surface for SEO + announcementsOptional; strong for organic growth
    • ✅ An onboarding flow, a public changelog, and a feedback channel are live (blog optional).
  2. 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 -5
    php artisan route:list | grep -i blog

    If routes exist and the editor is sufficient, write posts through the admin panel. If not, run a separate static blog (Astro or Hugo) at /blog for best SEO, or blog.<DOMAIN> for easiest deployment.

    OptionBest for
    AstroWindA polished marketing site plus blog
    AstroPaperA simple writer-first Astro blog
    Hugo-PaperModA 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 preview

    Hugo + PaperMod is the lean alternative when you want a fast static blog with fewer JavaScript moving parts:

    Terminal window
    hugo new site your-blog
    cd your-blog
    git init
    git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod
    printf 'theme = "PaperMod"\nbaseURL = "https://YOUR_DOMAIN/blog/"\n' >> hugo.toml
    hugo server
    • ✅ The blog, if chosen, is reachable and passes a mobile PageSpeed check.

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:

TypeBest for
Welcome modalSimple products, quick start
Guided tour / tooltipsComplex products, feature discovery
Onboarding checklistSaaS with setup steps
Email dripComplement 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.

  1. Create the migration — add a has_seen_welcome boolean column:

    Terminal window
    php artisan make:migration add_has_seen_welcome_to_users_table

    In the generated migration:

    $table->boolean('has_seen_welcome')->default(false);
    Terminal window
    php artisan migrate
  2. 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');
  3. Create the welcome modalresources/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
  4. Create the checklist widgetresources/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>
  5. Optional — add a guided tour with driver.js that fires only on the user’s first login.

    Terminal window
    npm install driver.js
    import { 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 same has_seen_welcome signal. Match selectors to real elements — do not invent IDs.

  6. 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>
    @endif

    Security: send opaque IDs only, enable HMAC user verification, add https://public.produktly.com to 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.

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.

  1. Create a Feature Requests board in ProductLift — log in → Boards → New board → name it “Feature Requests” → customise branding to match your app.

  2. Install the widget snippet — add the following before </body> in resources/views/layouts/app.blade.php:

    {{-- ProductLift Widget — ONLY after the user accepted functional/analytics
    consent (Phase 6 §6 gate). It ships id/email/name to a third party, so it
    must not load for a visitor who hasn't consented. Match the condition to
    your 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>
    @endif

    Set the widget ID via env (PRODUCTLIFT_ID) → config/services.php ('productlift' => ['id' => env('PRODUCTLIFT_ID')]) — never commit a literal ID inline. The @if gate must mirror whatever consent signal your Phase-6 banner writes.

  3. Commit:

    Terminal window
    git add resources/views/layouts/app.blade.php
    git 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.

Enable ProductLift’s Changelog module and create an initial “Welcome” entry so users can track what’s shipped.

  1. 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.

  2. Create your first entry — click Changelog → New post. Title: “Welcome to [App Name] — here’s what’s new.” Publish it.

  3. 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-changelog handler 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.

Do not mark this step done until every box below is checked.

  • 👤 Onboarding — first-run guidance so new users reach value fast.
  • 🤖 Onboarding — modal + migrationhas_seen_welcome column 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.