Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,788 @@
|
||||
# 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/)
|
||||
Reference in New Issue
Block a user