Skip to content

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

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;

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