At Elyvora US, we spent months building a library of 13 original research articles, 11 product comparison guides, and 52 individual product profiles, all focused on natural oral care.
The research was solid. The problem was access. Nobody's going to read 40,000+ words of content to figure out which toothpaste is right for them.
So we built a tool: the Oral Care Upgrader. Four questions, under 60 seconds, personalized top picks across 11 product categories.
Here's how the engineering works.
Architecture Decision: Client-Side Everything
The first decision was the most important: the entire scoring engine runs client-side. No API calls. No backend processing. No database queries at quiz time. Why?
- Privacy: We don't want user health data (age, dental concerns) touching our servers. Period. Your answers never leave your browser.
- Speed: A round-trip to compute recommendations would add latency that kills the "under 60 seconds" promise.
- Cost: Zero compute cost per user. The tool scales to a million users without our server bill changing.
- Resilience: No backend means no backend failures. The tool works even if our API is down.
The trade-off is bundle size, we ship all 52 product profiles to the client. But with Next.js code splitting and the data being ~15KB gzipped, it's a non-issue.
The Data Model
Each product is a typed object with fields that the scoring engine consumes:
interface OralCareProduct {
id: string;
slug: string; // matches our /products/[slug] route
title: string;
short: string; // display name
category: CategoryId; // one of 11 category identifiers
img: string; // product image path
score: number; // our editorial Elyvora US Score (1-10)
price: string; // display string ("$12.99")
priceAmount: number; // numeric for tier calculation
why: string; // default recommendation reason
tags: string[]; // scoring tags: the key to everything
}
The tags array is where the magic lives. Each product carries tags that map to specific user inputs:
tags: ['sensitivity', 'remineralizing', 'budget', 'sls-free',
'nha', 'age-50-plus', 'gentle', 'chemical-free']
These tags are hand-curated from our research, not auto-generated. When we say a product is tagged age-50-plus, it's because our research indicates the formulation specifically benefits aging oral tissue (gentle abrasives, high remineralization potential, no SLS irritation).
The Quiz: Structured Input Collection
The quiz is four steps, each collecting different signal types:
interface QuizStep {
id: 'age' | 'issues' | 'goals' | 'prefs';
title: string;
subtitle: string;
max: number; // max selections allowed
options: { id: string; label: string; icon: string }[];
}
- Age (single select), maps to biological frameworks.
- Concerns (multi-select, max 3), real problems users experienced.
- Goals (single select), real objectives users want to accomplish.
- Preferences (multi-select, max 3), to diversify.
Each option's id directly maps to product tags. This is the core design insight: the quiz options and product tags share a vocabulary. No translation layer needed.
The Scoring Algorithm
For each category, we score every product against the user's combined answer set:
function computeResults(allAnswers: string[]): CategoryResult[] {
return CATEGORIES.map(cat => {
const catProducts = getProductsByCategory(cat.id);
const scored = catProducts.map(product => {
// Base score from editorial rating
let score = product.score;
// Tag intersection scoring
const matchedTags = product.tags.filter(t =>
allAnswers.includes(t)
);
score += matchedTags.length * MATCH_WEIGHT;
// Build contextual "why" explanation
const whyText = assembleWhyText(product, matchedTags, allAnswers);
return { product, score, matchedTags, whyText };
});
// Sort by computed score, pick winner
scored.sort((a, b) => b.score - a.score);
return {
category: cat,
winner: scored[0].product,
scored: scored,
};
});
}
The actual implementation has more nuance (weighted tags for age-specific matches, bonus scoring for multi-signal alignment, tie-breaking by editorial score), but this is the skeleton.
The key insight: tag intersection + editorial baseline = good-enough personalization without ML. We don't need a neural network. We need a well-curated tag vocabulary and honest editorial scores.
Dynamic "Why" Text Generation
The hardest UX problem wasn't scoring, it was explaining the score. Users need to know WHY a product was recommended, or they won't trust it.
We solve this with template-based contextual assembly:
function assembleWhyText(
product: OralCareProduct,
matchedTags: string[],
answers: string[]
): string {
const reasons: string[] = [];
// Age-specific reason
if (matchedTags.some(t => t.startsWith('age-'))) {
reasons.push(AGE_REASONS[getAgeTag(answers)]);
}
// Issue-specific reasons
for (const tag of matchedTags) {
if (ISSUE_REASONS[tag]) {
reasons.push(ISSUE_REASONS[tag](product));
}
}
// Preference-specific reasons
for (const tag of matchedTags) {
if (PREF_REASONS[tag]) {
reasons.push(PREF_REASONS[tag](product));
}
}
// Combine top 3 reasons into coherent paragraph
return reasons.slice(0, 3).join(' ');
}
This produces output like:
"Formulated for mature enamel with high-concentration nano-hydroxyapatite for active remineralization. SLS-free formula avoids the mucosal irritation common in adults 50+. EWG Verified certification meets your preference for third-party validation."
Each sentence is contextual to the user's inputs. Same product, different user → different explanation.
Frontend: React + Framer Motion
The UI is a single React component with step-based state management:
step 0-3 → Quiz (4 screens)
step 4 → Results (11 category cards)
We use Framer Motion for step transitions and result card animations. The entire tool is a client component (use client) since it's purely interactive, no SSR benefit for a quiz.
Results are rendered as cards showing:
- Product image
- Elyvora US Score (gradient badge matching our product pages)
- Price tier badge (Budget / Mid-Range / Premium / High-End)
- Dynamic "Why this pick" explanation
- Links to full review, category comparison, and original research (so internal linking is solid)
Try It
The tool is live and free: Oral Care Upgrader
The entire codebase runs on Next.js 14 with TypeScript. No external APIs, no ML models, no user data collection. Just structured data and a tag-intersection algorithm that punches way above its weight class.
If you're building recommendation tools, the lesson is: you probably don't need AI. You need a good taxonomy and honest data.
Elyvora US is an independent product research publication. We publish comparison guides and original research on health, home, and tech products. Please subscribe for more content alike, and don't forget to leave a like and share this article with your friends. Cheers.
Top comments (0)