Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
789 lines
22 KiB
Markdown
789 lines
22 KiB
Markdown
# User Onboarding Flows
|
||
|
||
**Meta-Description:** User Onboarding für SaaS optimieren. Progressive Onboarding, Checklists, Product Tours und Activation Metrics.
|
||
|
||
**Keywords:** User Onboarding, Activation, Product Tour, Checklist, Time-to-Value, Retention, SaaS Onboarding
|
||
|
||
---
|
||
|
||
## Einführung
|
||
|
||
**User Onboarding** entscheidet über Trial-to-Paid Conversion und Retention. Der erste Eindruck zählt – **40-60% der Free Trial User** kehren nach dem ersten Tag nie zurück. Dieser Guide zeigt effektive Onboarding-Strategien.
|
||
|
||
---
|
||
|
||
## Onboarding Overview
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ USER ONBOARDING JOURNEY │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Onboarding Phases: │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ 1. SIGNUP FLOW: │ │
|
||
│ │ ├── Minimal friction │ │
|
||
│ │ ├── Social login options │ │
|
||
│ │ └── Welcome message │ │
|
||
│ │ ↓ │ │
|
||
│ │ 2. INITIAL SETUP: │ │
|
||
│ │ ├── Profile completion │ │
|
||
│ │ ├── Preferences selection │ │
|
||
│ │ └── Team invitation (if B2B) │ │
|
||
│ │ ↓ │ │
|
||
│ │ 3. FIRST VALUE: │ │
|
||
│ │ ├── Guided first action │ │
|
||
│ │ ├── Quick win celebration │ │
|
||
│ │ └── "Aha moment" trigger │ │
|
||
│ │ ↓ │ │
|
||
│ │ 4. ACTIVATION: │ │
|
||
│ │ ├── Core features used │ │
|
||
│ │ ├── Habit formation │ │
|
||
│ │ └── Ready for conversion │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Key Metrics: │
|
||
│ ├── Time to First Value (TTFV) │
|
||
│ ├── Activation Rate │
|
||
│ ├── Onboarding Completion Rate │
|
||
│ ├── Day 1/7/30 Retention │
|
||
│ └── Trial-to-Paid Conversion │
|
||
│ │
|
||
│ Activation Events (Example): │
|
||
│ ├── Created first project │
|
||
│ ├── Invited team member │
|
||
│ ├── Connected integration │
|
||
│ └── Completed first workflow │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Onboarding Checklist
|
||
|
||
```typescript
|
||
// lib/onboarding/types.ts
|
||
export interface OnboardingStep {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
action: {
|
||
type: 'link' | 'modal' | 'trigger';
|
||
target: string;
|
||
};
|
||
completionEvent: string;
|
||
points?: number;
|
||
required?: boolean;
|
||
order: number;
|
||
}
|
||
|
||
export interface UserOnboarding {
|
||
userId: string;
|
||
completedSteps: string[];
|
||
currentStep: string | null;
|
||
startedAt: Date;
|
||
completedAt: Date | null;
|
||
skippedAt: Date | null;
|
||
}
|
||
|
||
// lib/onboarding/config.ts
|
||
export const onboardingSteps: OnboardingStep[] = [
|
||
{
|
||
id: 'complete-profile',
|
||
title: 'Complete your profile',
|
||
description: 'Add your name and profile picture',
|
||
action: { type: 'link', target: '/settings/profile' },
|
||
completionEvent: 'profile_completed',
|
||
points: 10,
|
||
required: true,
|
||
order: 1
|
||
},
|
||
{
|
||
id: 'create-project',
|
||
title: 'Create your first project',
|
||
description: 'Set up a project to organize your work',
|
||
action: { type: 'modal', target: 'create-project-modal' },
|
||
completionEvent: 'project_created',
|
||
points: 20,
|
||
required: true,
|
||
order: 2
|
||
},
|
||
{
|
||
id: 'invite-team',
|
||
title: 'Invite your team',
|
||
description: 'Collaborate with your colleagues',
|
||
action: { type: 'link', target: '/settings/team' },
|
||
completionEvent: 'team_member_invited',
|
||
points: 15,
|
||
required: false,
|
||
order: 3
|
||
},
|
||
{
|
||
id: 'connect-integration',
|
||
title: 'Connect an integration',
|
||
description: 'Sync with your favorite tools',
|
||
action: { type: 'link', target: '/settings/integrations' },
|
||
completionEvent: 'integration_connected',
|
||
points: 20,
|
||
required: false,
|
||
order: 4
|
||
},
|
||
{
|
||
id: 'complete-workflow',
|
||
title: 'Complete your first workflow',
|
||
description: 'Experience the full power of our platform',
|
||
action: { type: 'trigger', target: 'start-workflow-tour' },
|
||
completionEvent: 'workflow_completed',
|
||
points: 35,
|
||
required: true,
|
||
order: 5
|
||
}
|
||
];
|
||
|
||
export const ACTIVATION_THRESHOLD = 3; // Steps to be "activated"
|
||
```
|
||
|
||
```typescript
|
||
// lib/onboarding/service.ts
|
||
import { db } from '@/lib/db';
|
||
import { onboardingSteps, ACTIVATION_THRESHOLD } from './config';
|
||
import { track } from '@/lib/analytics';
|
||
|
||
export async function getOnboardingProgress(userId: string) {
|
||
const onboarding = await db.userOnboarding.findUnique({
|
||
where: { userId }
|
||
});
|
||
|
||
if (!onboarding) {
|
||
// Initialize onboarding
|
||
return db.userOnboarding.create({
|
||
data: {
|
||
userId,
|
||
completedSteps: [],
|
||
currentStep: onboardingSteps[0].id,
|
||
startedAt: new Date()
|
||
}
|
||
});
|
||
}
|
||
|
||
return onboarding;
|
||
}
|
||
|
||
export async function completeOnboardingStep(
|
||
userId: string,
|
||
stepId: string
|
||
): Promise<{ completed: boolean; isActivated: boolean; nextStep: string | null }> {
|
||
const onboarding = await getOnboardingProgress(userId);
|
||
|
||
if (onboarding.completedSteps.includes(stepId)) {
|
||
return {
|
||
completed: false,
|
||
isActivated: isUserActivated(onboarding.completedSteps),
|
||
nextStep: onboarding.currentStep
|
||
};
|
||
}
|
||
|
||
const step = onboardingSteps.find(s => s.id === stepId);
|
||
if (!step) {
|
||
throw new Error(`Unknown step: ${stepId}`);
|
||
}
|
||
|
||
const completedSteps = [...onboarding.completedSteps, stepId];
|
||
const nextStep = getNextStep(completedSteps);
|
||
const isCompleted = nextStep === null;
|
||
|
||
await db.userOnboarding.update({
|
||
where: { userId },
|
||
data: {
|
||
completedSteps,
|
||
currentStep: nextStep,
|
||
...(isCompleted && { completedAt: new Date() })
|
||
}
|
||
});
|
||
|
||
// Track analytics
|
||
track('onboarding_step_completed', {
|
||
userId,
|
||
stepId,
|
||
stepTitle: step.title,
|
||
points: step.points,
|
||
totalCompleted: completedSteps.length,
|
||
isActivated: isUserActivated(completedSteps)
|
||
});
|
||
|
||
// Award points (gamification)
|
||
if (step.points) {
|
||
await awardPoints(userId, step.points, `Completed: ${step.title}`);
|
||
}
|
||
|
||
// Check activation
|
||
const activated = isUserActivated(completedSteps);
|
||
if (activated && !isUserActivated(onboarding.completedSteps)) {
|
||
await handleUserActivation(userId);
|
||
}
|
||
|
||
return {
|
||
completed: true,
|
||
isActivated: activated,
|
||
nextStep
|
||
};
|
||
}
|
||
|
||
function getNextStep(completedSteps: string[]): string | null {
|
||
const remaining = onboardingSteps
|
||
.filter(s => !completedSteps.includes(s.id))
|
||
.sort((a, b) => a.order - b.order);
|
||
|
||
return remaining[0]?.id || null;
|
||
}
|
||
|
||
function isUserActivated(completedSteps: string[]): boolean {
|
||
const requiredSteps = onboardingSteps.filter(s => s.required);
|
||
const completedRequired = requiredSteps.filter(s =>
|
||
completedSteps.includes(s.id)
|
||
);
|
||
|
||
return completedRequired.length >= ACTIVATION_THRESHOLD;
|
||
}
|
||
|
||
async function handleUserActivation(userId: string) {
|
||
// Update user status
|
||
await db.user.update({
|
||
where: { id: userId },
|
||
data: { isActivated: true, activatedAt: new Date() }
|
||
});
|
||
|
||
// Track activation
|
||
track('user_activated', { userId });
|
||
|
||
// Send activation email
|
||
await sendActivationEmail(userId);
|
||
|
||
// Notify sales (for high-value leads)
|
||
await notifySalesIfQualified(userId);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Onboarding UI Components
|
||
|
||
```tsx
|
||
// components/onboarding/OnboardingChecklist.tsx
|
||
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { CheckCircle, Circle, ChevronRight, X } from 'lucide-react';
|
||
import { onboardingSteps, OnboardingStep } from '@/lib/onboarding/config';
|
||
|
||
interface OnboardingProgress {
|
||
completedSteps: string[];
|
||
currentStep: string | null;
|
||
}
|
||
|
||
export function OnboardingChecklist() {
|
||
const [progress, setProgress] = useState<OnboardingProgress | null>(null);
|
||
const [isExpanded, setIsExpanded] = useState(true);
|
||
const [isDismissed, setIsDismissed] = useState(false);
|
||
|
||
useEffect(() => {
|
||
fetch('/api/onboarding/progress')
|
||
.then(res => res.json())
|
||
.then(setProgress);
|
||
}, []);
|
||
|
||
if (!progress || isDismissed) return null;
|
||
|
||
const completedCount = progress.completedSteps.length;
|
||
const totalCount = onboardingSteps.length;
|
||
const progressPercent = (completedCount / totalCount) * 100;
|
||
|
||
// Hide if completed
|
||
if (completedCount === totalCount) return null;
|
||
|
||
return (
|
||
<div className="fixed bottom-4 right-4 w-80 bg-white rounded-lg shadow-lg border">
|
||
{/* Header */}
|
||
<div
|
||
className="flex items-center justify-between p-4 cursor-pointer"
|
||
onClick={() => setIsExpanded(!isExpanded)}
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<div className="relative w-10 h-10">
|
||
<svg className="w-10 h-10 -rotate-90">
|
||
<circle
|
||
cx="20" cy="20" r="16"
|
||
fill="none"
|
||
stroke="#e5e7eb"
|
||
strokeWidth="4"
|
||
/>
|
||
<circle
|
||
cx="20" cy="20" r="16"
|
||
fill="none"
|
||
stroke="#3b82f6"
|
||
strokeWidth="4"
|
||
strokeDasharray={`${progressPercent} 100`}
|
||
strokeLinecap="round"
|
||
/>
|
||
</svg>
|
||
<span className="absolute inset-0 flex items-center justify-center text-sm font-semibold">
|
||
{completedCount}/{totalCount}
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<h3 className="font-semibold">Getting Started</h3>
|
||
<p className="text-sm text-gray-500">
|
||
{completedCount === 0 ? "Let's set you up!" : `${totalCount - completedCount} steps left`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setIsDismissed(true); }}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Steps */}
|
||
{isExpanded && (
|
||
<div className="border-t max-h-80 overflow-y-auto">
|
||
{onboardingSteps.map((step) => (
|
||
<OnboardingStepItem
|
||
key={step.id}
|
||
step={step}
|
||
isCompleted={progress.completedSteps.includes(step.id)}
|
||
isCurrent={progress.currentStep === step.id}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function OnboardingStepItem({
|
||
step,
|
||
isCompleted,
|
||
isCurrent
|
||
}: {
|
||
step: OnboardingStep;
|
||
isCompleted: boolean;
|
||
isCurrent: boolean;
|
||
}) {
|
||
const handleClick = () => {
|
||
if (isCompleted) return;
|
||
|
||
if (step.action.type === 'link') {
|
||
window.location.href = step.action.target;
|
||
} else if (step.action.type === 'modal') {
|
||
// Dispatch event to open modal
|
||
window.dispatchEvent(new CustomEvent('open-modal', {
|
||
detail: { id: step.action.target }
|
||
}));
|
||
} else if (step.action.type === 'trigger') {
|
||
window.dispatchEvent(new CustomEvent(step.action.target));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<button
|
||
onClick={handleClick}
|
||
disabled={isCompleted}
|
||
className={`w-full flex items-center gap-3 p-4 text-left hover:bg-gray-50 transition ${
|
||
isCompleted ? 'opacity-60' : ''
|
||
} ${isCurrent ? 'bg-blue-50' : ''}`}
|
||
>
|
||
{isCompleted ? (
|
||
<CheckCircle className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||
) : (
|
||
<Circle className={`w-5 h-5 flex-shrink-0 ${isCurrent ? 'text-blue-500' : 'text-gray-300'}`} />
|
||
)}
|
||
<div className="flex-1 min-w-0">
|
||
<p className={`font-medium ${isCompleted ? 'line-through' : ''}`}>
|
||
{step.title}
|
||
</p>
|
||
<p className="text-sm text-gray-500 truncate">
|
||
{step.description}
|
||
</p>
|
||
</div>
|
||
{!isCompleted && (
|
||
<ChevronRight className="w-5 h-5 text-gray-400 flex-shrink-0" />
|
||
)}
|
||
</button>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Product Tour
|
||
|
||
```tsx
|
||
// components/onboarding/ProductTour.tsx
|
||
'use client';
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
|
||
interface TourStep {
|
||
target: string; // CSS selector
|
||
title: string;
|
||
content: string;
|
||
placement: 'top' | 'bottom' | 'left' | 'right';
|
||
action?: {
|
||
label: string;
|
||
onClick: () => void;
|
||
};
|
||
}
|
||
|
||
interface ProductTourProps {
|
||
tourId: string;
|
||
steps: TourStep[];
|
||
onComplete: () => void;
|
||
onSkip?: () => void;
|
||
}
|
||
|
||
export function ProductTour({
|
||
tourId,
|
||
steps,
|
||
onComplete,
|
||
onSkip
|
||
}: ProductTourProps) {
|
||
const [currentStep, setCurrentStep] = useState(0);
|
||
const [targetRect, setTargetRect] = useState<DOMRect | null>(null);
|
||
|
||
const step = steps[currentStep];
|
||
|
||
// Find and highlight target element
|
||
useEffect(() => {
|
||
const target = document.querySelector(step.target);
|
||
if (target) {
|
||
setTargetRect(target.getBoundingClientRect());
|
||
|
||
// Scroll into view
|
||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
|
||
// Add highlight class
|
||
target.classList.add('tour-highlight');
|
||
|
||
return () => {
|
||
target.classList.remove('tour-highlight');
|
||
};
|
||
}
|
||
}, [step.target]);
|
||
|
||
const handleNext = useCallback(() => {
|
||
if (currentStep < steps.length - 1) {
|
||
setCurrentStep(currentStep + 1);
|
||
} else {
|
||
onComplete();
|
||
}
|
||
}, [currentStep, steps.length, onComplete]);
|
||
|
||
const handlePrev = () => {
|
||
if (currentStep > 0) {
|
||
setCurrentStep(currentStep - 1);
|
||
}
|
||
};
|
||
|
||
const handleSkip = () => {
|
||
onSkip?.();
|
||
};
|
||
|
||
if (!targetRect) return null;
|
||
|
||
// Calculate tooltip position
|
||
const tooltipStyle = calculateTooltipPosition(targetRect, step.placement);
|
||
|
||
return createPortal(
|
||
<>
|
||
{/* Overlay with spotlight */}
|
||
<div className="fixed inset-0 z-50">
|
||
<svg className="w-full h-full">
|
||
<defs>
|
||
<mask id="spotlight-mask">
|
||
<rect width="100%" height="100%" fill="white" />
|
||
<rect
|
||
x={targetRect.x - 8}
|
||
y={targetRect.y - 8}
|
||
width={targetRect.width + 16}
|
||
height={targetRect.height + 16}
|
||
rx="8"
|
||
fill="black"
|
||
/>
|
||
</mask>
|
||
</defs>
|
||
<rect
|
||
width="100%"
|
||
height="100%"
|
||
fill="rgba(0,0,0,0.5)"
|
||
mask="url(#spotlight-mask)"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
|
||
{/* Tooltip */}
|
||
<div
|
||
className="fixed z-50 w-80 bg-white rounded-lg shadow-xl p-4"
|
||
style={tooltipStyle}
|
||
>
|
||
{/* Progress */}
|
||
<div className="flex gap-1 mb-3">
|
||
{steps.map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className={`h-1 flex-1 rounded ${
|
||
i <= currentStep ? 'bg-blue-500' : 'bg-gray-200'
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
<h3 className="font-semibold text-lg mb-2">{step.title}</h3>
|
||
<p className="text-gray-600 mb-4">{step.content}</p>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center justify-between">
|
||
<button
|
||
onClick={handleSkip}
|
||
className="text-sm text-gray-500 hover:text-gray-700"
|
||
>
|
||
Skip tour
|
||
</button>
|
||
<div className="flex gap-2">
|
||
{currentStep > 0 && (
|
||
<button
|
||
onClick={handlePrev}
|
||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||
>
|
||
Back
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={handleNext}
|
||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||
>
|
||
{currentStep === steps.length - 1 ? 'Finish' : 'Next'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>,
|
||
document.body
|
||
);
|
||
}
|
||
|
||
function calculateTooltipPosition(
|
||
targetRect: DOMRect,
|
||
placement: string
|
||
): React.CSSProperties {
|
||
const offset = 16;
|
||
|
||
switch (placement) {
|
||
case 'bottom':
|
||
return {
|
||
top: targetRect.bottom + offset,
|
||
left: targetRect.left + targetRect.width / 2,
|
||
transform: 'translateX(-50%)'
|
||
};
|
||
case 'top':
|
||
return {
|
||
bottom: window.innerHeight - targetRect.top + offset,
|
||
left: targetRect.left + targetRect.width / 2,
|
||
transform: 'translateX(-50%)'
|
||
};
|
||
case 'right':
|
||
return {
|
||
top: targetRect.top + targetRect.height / 2,
|
||
left: targetRect.right + offset,
|
||
transform: 'translateY(-50%)'
|
||
};
|
||
case 'left':
|
||
return {
|
||
top: targetRect.top + targetRect.height / 2,
|
||
right: window.innerWidth - targetRect.left + offset,
|
||
transform: 'translateY(-50%)'
|
||
};
|
||
default:
|
||
return {};
|
||
}
|
||
}
|
||
|
||
// CSS for highlighting
|
||
/*
|
||
.tour-highlight {
|
||
position: relative;
|
||
z-index: 51;
|
||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.5);
|
||
border-radius: 8px;
|
||
}
|
||
*/
|
||
```
|
||
|
||
---
|
||
|
||
## Activation Tracking
|
||
|
||
```typescript
|
||
// lib/onboarding/activation.ts
|
||
import { db } from '@/lib/db';
|
||
import { track } from '@/lib/analytics';
|
||
|
||
interface ActivationEvent {
|
||
userId: string;
|
||
event: string;
|
||
metadata?: Record<string, unknown>;
|
||
}
|
||
|
||
// Define activation criteria
|
||
const ACTIVATION_EVENTS = [
|
||
{ event: 'project_created', weight: 2 },
|
||
{ event: 'team_member_invited', weight: 1 },
|
||
{ event: 'integration_connected', weight: 2 },
|
||
{ event: 'workflow_completed', weight: 3 },
|
||
{ event: 'dashboard_customized', weight: 1 }
|
||
];
|
||
|
||
const ACTIVATION_THRESHOLD = 5; // Total weight needed
|
||
|
||
export async function trackActivationEvent(
|
||
event: ActivationEvent
|
||
): Promise<{ isActivated: boolean; progress: number }> {
|
||
const { userId, event: eventName, metadata } = event;
|
||
|
||
// Record the event
|
||
await db.userEvent.create({
|
||
data: {
|
||
userId,
|
||
eventName,
|
||
metadata: metadata || {},
|
||
occurredAt: new Date()
|
||
}
|
||
});
|
||
|
||
// Calculate activation progress
|
||
const userEvents = await db.userEvent.findMany({
|
||
where: { userId },
|
||
select: { eventName: true }
|
||
});
|
||
|
||
const uniqueEvents = new Set(userEvents.map(e => e.eventName));
|
||
|
||
let totalWeight = 0;
|
||
ACTIVATION_EVENTS.forEach(ae => {
|
||
if (uniqueEvents.has(ae.event)) {
|
||
totalWeight += ae.weight;
|
||
}
|
||
});
|
||
|
||
const progress = Math.min(100, (totalWeight / ACTIVATION_THRESHOLD) * 100);
|
||
const isActivated = totalWeight >= ACTIVATION_THRESHOLD;
|
||
|
||
// Check if user just became activated
|
||
const user = await db.user.findUnique({
|
||
where: { id: userId },
|
||
select: { isActivated: true }
|
||
});
|
||
|
||
if (isActivated && !user?.isActivated) {
|
||
await activateUser(userId);
|
||
}
|
||
|
||
return { isActivated, progress };
|
||
}
|
||
|
||
async function activateUser(userId: string) {
|
||
const activationTime = await calculateTimeToActivation(userId);
|
||
|
||
await db.user.update({
|
||
where: { id: userId },
|
||
data: {
|
||
isActivated: true,
|
||
activatedAt: new Date()
|
||
}
|
||
});
|
||
|
||
track('user_activated', {
|
||
userId,
|
||
timeToActivation: activationTime
|
||
});
|
||
|
||
// Trigger post-activation flow
|
||
await triggerPostActivation(userId);
|
||
}
|
||
|
||
async function calculateTimeToActivation(userId: string): Promise<number> {
|
||
const user = await db.user.findUnique({
|
||
where: { id: userId },
|
||
select: { createdAt: true }
|
||
});
|
||
|
||
if (!user) return 0;
|
||
|
||
return Date.now() - user.createdAt.getTime();
|
||
}
|
||
|
||
async function triggerPostActivation(userId: string) {
|
||
// Send congratulations email
|
||
await sendEmail(userId, 'activation-congrats');
|
||
|
||
// Schedule upgrade prompt
|
||
await scheduleEmail(userId, 'upgrade-prompt', { delay: '3d' });
|
||
|
||
// Notify sales for qualified leads
|
||
const user = await db.user.findUnique({
|
||
where: { id: userId },
|
||
include: { tenant: { include: { subscription: true } } }
|
||
});
|
||
|
||
if (user?.tenant && isQualifiedLead(user.tenant)) {
|
||
await notifySales(userId, 'activated_qualified_lead');
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Best Practices
|
||
|
||
| Aspect | Recommendation |
|
||
|--------|----------------|
|
||
| **Time to Value** | < 5 minutes to first success |
|
||
| **Steps** | Max 5-7 onboarding steps |
|
||
| **Progress** | Always show completion % |
|
||
| **Personalization** | Tailor based on use case |
|
||
| **Help** | Offer chat/support at each step |
|
||
| **Celebration** | Acknowledge completions |
|
||
|
||
---
|
||
|
||
## Fazit
|
||
|
||
Effektives User Onboarding:
|
||
|
||
1. **Progressive Disclosure**: Nicht alles auf einmal zeigen
|
||
2. **Quick Wins**: Erste Erfolge ermöglichen
|
||
3. **Tracking**: Activation Events messen
|
||
4. **Iteration**: Kontinuierlich verbessern
|
||
|
||
Gutes Onboarding ist der Schlüssel zu Retention.
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "Onboarding checklist UI, progress bar and completed steps"
|
||
2. "Product tour spotlight on feature, tooltip explanation"
|
||
3. "User activation funnel, stages from signup to activated"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [Appcues Onboarding Guide](https://www.appcues.com/blog/user-onboarding)
|
||
- [UserPilot Onboarding](https://userpilot.com/blog/user-onboarding/)
|
||
- [Product-Led Growth Onboarding](https://www.productled.org/foundations/user-onboarding)
|
||
- [Intercom Onboarding](https://www.intercom.com/blog/user-onboarding-best-practices/)
|