Content Platform — Personalization¶
User segments, content recommendations, A/B testing, and personalized experiences. Personalization is OPTIONAL for most content platforms — scope it explicitly.
When Personalization Applies¶
IF: Client's business model depends on repeat visits and engagement THEN: Scope personalization features
IF: Client's content volume is < 50 articles THEN: Skip personalization — not enough content to personalize
IF: Client has no user accounts THEN: Limit to anonymous personalization (cookie-based, no PII)
User Segments¶
Segment Types¶
OPTIONAL: Anonymous visitor — no account, identified by cookie OPTIONAL: Registered reader — has account, reading history tracked OPTIONAL: Subscriber — paying or newsletter-subscribed OPTIONAL: Contributor — authors content, different editorial view
Behavioral Segments¶
OPTIONAL: New visitor — first session, no reading history OPTIONAL: Returning reader — 3+ visits in last 30 days OPTIONAL: Category enthusiast — 5+ articles read in same category OPTIONAL: Deep reader — average scroll depth > 80% OPTIONAL: Bouncer — single page view, < 10 seconds
Segment Implementation¶
SCOPE_ITEM: Segments derived from first-party behavioral data only SCOPE_ITEM: No third-party tracking pixels or cookies COMPLIANCE: GDPR — segments based on anonymous behavioral data, no PII without consent COMPLIANCE: Cookie consent required before setting any non-essential cookies
// Segment assignment — middleware or server component
function deriveSegment(sessionData: SessionData): UserSegment {
if (sessionData.visitCount === 1) return 'new_visitor';
if (sessionData.articlesRead > 10) return 'power_reader';
if (sessionData.topCategory && sessionData.categoryReads > 5) {
return `enthusiast:${sessionData.topCategory}`;
}
return 'returning_reader';
}
Content Recommendations¶
Related Content (Tag/Category Based)¶
SCOPE_ITEM: "Related articles" on every article page SCOPE_ITEM: Match by shared categories and tags, weighted by overlap count SCOPE_ITEM: Exclude current article from results SCOPE_ITEM: Fall back to "Most popular in category" if insufficient matches
-- Related articles by shared tags (ranked by overlap)
SELECT a.id, a.title, a.slug, COUNT(shared.tag_id) AS relevance
FROM articles a
JOIN article_tags shared ON shared.article_id = a.id
WHERE shared.tag_id IN (
SELECT tag_id FROM article_tags WHERE article_id = :current_article_id
)
AND a.id != :current_article_id
AND a.status = 'published'
GROUP BY a.id
ORDER BY relevance DESC, a.published_at DESC
LIMIT 4;
Popular Content¶
SCOPE_ITEM: "Most read" widget based on view count (last 7 or 30 days) OPTIONAL: "Trending" based on engagement velocity (views per hour, last 24h)
Personalized Recommendations¶
IF: Client has user accounts with reading history THEN: Recommend based on user's reading patterns
OPTIONAL: Collaborative filtering — readers who read X also read Y OPTIONAL: Content-based filtering — match article embeddings to user profile OPTIONAL: Recency bias — prefer newer content in recommendations
IF: Collaborative filtering needed THEN: Precompute recommendation matrix nightly (batch job)
IF: Real-time recommendations needed THEN: Edge function with cached user profile + pre-ranked content lists
A/B Testing¶
Architecture¶
OPTIONAL: Middleware-based variant assignment OPTIONAL: Cookie stores variant assignment (sticky sessions) OPTIONAL: Analytics events track conversion per variant
Implementation Pattern¶
// middleware.ts — A/B test assignment
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const existingVariant = request.cookies.get('ab_headline_test');
if (!existingVariant) {
const variant = Math.random() < 0.5 ? 'a' : 'b';
response.cookies.set('ab_headline_test', variant, {
maxAge: 60 * 60 * 24 * 30, // 30 days
httpOnly: false, // readable by client analytics
sameSite: 'lax',
});
}
return response;
}
What to A/B Test on Content Platforms¶
OPTIONAL: Headlines — two title variants, measure click-through rate OPTIONAL: Layout — list vs. grid on archive pages, measure engagement OPTIONAL: CTA placement — newsletter signup position, measure conversion OPTIONAL: Content length — short excerpt vs. long preview, measure read-through
A/B Testing Rules¶
CHECK: Minimum sample size before declaring winner (1,000+ visitors per variant) CHECK: Run tests for at least 2 full weeks (capture weekday/weekend patterns) CHECK: One test per page at a time — no overlapping experiments CHECK: Statistical significance required (p < 0.05) before concluding
Reading History¶
Tracking¶
OPTIONAL: Store reading history per authenticated user OPTIONAL: Track article ID, timestamp, scroll depth, time on page OPTIONAL: Reading history accessible in user profile
CREATE TABLE reading_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
article_id UUID REFERENCES articles(id) ON DELETE CASCADE,
read_at TIMESTAMPTZ NOT NULL DEFAULT now(),
scroll_depth REAL, -- 0.0 to 1.0
time_on_page INTEGER, -- seconds
UNIQUE(user_id, article_id) -- one entry per user per article
);
COMPLIANCE: Reading history is personal data — requires explicit consent under GDPR COMPLIANCE: User can request deletion of reading history (right to erasure) COMPLIANCE: Retention policy — auto-delete reading history older than 12 months
Saved / Bookmarked Content¶
OPTIONAL: "Save for later" button on article cards and pages OPTIONAL: Saved articles list in user profile OPTIONAL: Sync across devices (tied to user account)
IF: Client has no user accounts THEN: Implement bookmarks with localStorage (no server sync)
IF: Client has user accounts THEN: Store bookmarks in PostgreSQL, sync across sessions
Email Digest Personalization¶
IF: Client has newsletter with user segments THEN: Personalize digest content per subscriber
OPTIONAL: Weekly digest with articles from followed categories OPTIONAL: "You might have missed" section based on reading gaps OPTIONAL: Engagement-based frequency — active readers get weekly, inactive get monthly
Implementation¶
OPTIONAL: Nightly job builds per-user content selections OPTIONAL: Brevo (FR, preferred) or Mailchimp (US, secondary — sovereignty risk) merge tags for dynamic content blocks OPTIONAL: Unsubscribe link per digest type (not global unsubscribe)
Personalization and Caching¶
The Cache Problem¶
IF: Content is personalized THEN: Page cannot be fully cached at CDN level
Solutions¶
IF: Personalization is lightweight (recommendations sidebar) THEN: Use PPR — static article shell + streaming personalized sidebar
IF: Personalization affects main content (different headline per segment) THEN: Use edge middleware to set variant cookie, render via RSC
IF: Personalization is client-only (saved state, reading progress) THEN: SSG/ISR for page, hydrate personalization client-side
CHECK: Never server-render PII into cached HTML
CHECK: Personalized responses must have Cache-Control: private or Vary header
CHECK: Test that CDN does not serve User A's personalized page to User B
Privacy-First Personalization Principles¶
COMPLIANCE: First-party data only — no third-party tracking COMPLIANCE: Cookie consent before any non-essential tracking COMPLIANCE: Anonymous segments do not constitute personal data (no PII linkage) COMPLIANCE: Authenticated personalization requires explicit opt-in COMPLIANCE: User can disable personalization and see default content COMPLIANCE: Data retention limits on behavioral data (12 months maximum)
CHECK: Privacy policy describes personalization data usage CHECK: Cookie banner lists personalization cookies CHECK: Personalization degrades gracefully when cookies are blocked