# GlyphDiff.com - Font Comparison Project Plan > **Status:** Learning-Focused MVP | **Tech Stack:** SvelteKit + TypeScript + Tailwind CSS | **Transition Path:** React → Svelte --- ## Table of Contents 1. [Project Overview](#project-overview) 2. [Executive Summary](#executive-summary) 3. [Architecture Decisions](#architecture-decisions) 4. [Font Provider Integration Strategy](#font-provider-integration-strategy) 5. [Performance Optimization](#performance-optimization) 6. [Frontend Architecture](#frontend-architecture) 7. [Data Models](#data-models) 8. [Implementation Roadmap](#implementation-roadmap) 9. [TanStack Query Research](#tanstack-query-research) 10. [Learning Guide: React → Svelte](#learning-guide-react--svelte) 11. [Scalability & Commercialization](#scalability--commercialization) 12. [Deployment](#deployment) 13. [Risk Assessment](#risk-assessment) 14. [Success Metrics](#success-metrics) 15. [Appendix](#appendix) --- ## Project Overview ### Vision Create a modern, fast, and intuitive web application for comparing fonts side-by-side. The tool will help designers, developers, and typographers make informed decisions about font choices by providing real-time visual comparisons, filtering, and customization options. ### Target Audience - **UI/UX Designers:** Selecting typefaces for web and mobile applications - **Web Developers:** Choosing fonts for implementation in projects - **Graphic Designers:** Finding complementary fonts for branding - **Typography Enthusiasts:** Exploring and comparing font characteristics ### Core Features (MVP) 1. **Font Catalog:** Browse available fonts from multiple providers 2. **Side-by-Side Comparison:** Compare up to 4 fonts simultaneously 3. **Custom Preview Text:** Enter custom text for realistic comparison 4. **Filter & Search:** Filter by category, popularity, weight, width 5. **Responsive Design:** Seamless experience across all devices 6. **Dark Mode:** Built-in theme toggle for comfortable viewing 7. **URL Sharing:** Share comparison sessions via shareable URLs ### Tech Stack | Layer | Technology | Purpose | |-------|-----------|---------| | Framework | SvelteKit 2.x | Full-stack framework with SSR/SSG support | | UI Language | Svelte 5 | Reactive component framework | | Styling | Tailwind CSS 4.x | Utility-first CSS framework | | Component Library | Bits UI | Accessible Svelte components (Radix UI port) | | Type Safety | TypeScript 5.x | Static type checking | | Package Manager | Yarn | Fast, reliable dependency management | | Linting | oxlint | Fast Rust-based linter | | Formatting | dprint | Fast Rust-based code formatter | | Hosting | Vercel | Edge deployment & preview environments | ### Project Goals #### Learning Goals (Primary) - Gain proficiency in Svelte 5 and its reactive primitives (`$state`, `$derived`, `$effect`) - Understand SvelteKit's routing and data loading architecture - Learn client-side state management patterns in Svelte ecosystem - Explore performance optimization techniques for static web apps #### Project Goals (Secondary) - Build a functional, visually appealing font comparison tool - Create a performant, accessible user interface - Establish a foundation for future commercialization - Demonstrate mastery of modern frontend development practices --- ## Executive Summary ### Recommended Approach: Pure Client-Side Architecture **Recommendation:** Build the MVP as a pure client-side application using SvelteKit's static site generation (SSG) capabilities, with no backend infrastructure. ### Key Rationale 1. **Learning Focus:** Minimize backend complexity to concentrate on Svelte/SvelteKit mastery 2. **Data Availability:** Font metadata is publicly available via Google Fonts API and Fontshare CDN 3. **Performance:** Static generation provides instant page loads and optimal Core Web Vitals 4. **Cost Efficiency:** No server costs during MVP phase 5. **Future Flexibility:** Can easily add backend later when user data collection becomes necessary ### Architecture at a Glance ``` ┌─────────────────────────────────────────────────────────────┐ │ User Browser │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ SvelteKit Client Application │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Font Store │ │ Filter Store│ │ Comp Store │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ Font Provider Services │ │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ │ │ Google Fonts │ │ Fontshare │ │ │ │ │ │ │ │ API │ │ CDN │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ │ │ HTTP Requests │ CSS Font Files ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ Google Fonts API │ │ Fontshare CDN │ └──────────────────┘ └──────────────────┘ ``` --- ## Architecture Decisions ### Backend Necessity Analysis #### Option A: Pure Client-Side (Recommended for MVP) **Pros:** - **Simplicity:** No server code, database, or API endpoints to maintain - **Performance:** Static sites load faster and score better on Core Web Vitals - **Cost:** Zero hosting costs on Vercel free tier - **Learning Focus:** Maximum time spent on Svelte/SvelteKit fundamentals - **Deployment:** Instant deployments with zero configuration - **Scalability:** Vercel Edge handles CDN distribution automatically **Cons:** - **No User Data:** Cannot save user preferences or comparison history - **No Analytics:** Limited to client-side analytics (e.g., Google Analytics) - **Rate Limiting:** Google Fonts API has usage limits (1,000 requests/day) - **Caching:** Cannot implement server-side caching strategies - **Data Freshness:** Font metadata fetched on each session (mitigated by browser caching) **Use Cases This Supports:** - ✅ Browse and compare fonts - ✅ Share comparisons via URL - ✅ Filter and search fonts - ✅ Save preferences locally (localStorage) - ✅ Responsive, dark mode UI **Use Cases This Does NOT Support:** - ❌ User accounts and saved comparisons - ❌ Custom font uploads - ❌ Collaborative comparisons - ❌ Advanced analytics --- #### Option B: Hybrid with Serverless Backend **Pros:** - **Caching Layer:** Server-side caching reduces API calls and improves load times - **Rate Limit Management:** Proxy requests to avoid Google Fonts API limits - **User Data:** Save user preferences and comparison history - **Custom Endpoints:** Create specialized API endpoints for font data - **Preprocessing:** Transform and normalize font data before sending to client **Cons:** - **Complexity:** Additional infrastructure to build and maintain - **Cost:** Vercel Pro plan may be required for serverless functions - **Latency:** Server round-trip adds latency to initial page load - **Development Overhead:** Testing backend code requires additional considerations - **Learning Curve:** Takes time away from Svelte learning goals **Recommended Backend Stack (if needed later):** - **API Routes:** SvelteKit server endpoints (`+page.server.ts`, `+server.ts`) - **Database:** Vercel Postgres (Neon) or SQLite (better-sqlite3) - **Caching:** Vercel KV (Redis) or in-memory caching - **Auth:** Lucia Auth or Clerk for user authentication --- ### Recommendation **Build Option A (Pure Client-Side) for MVP with these considerations:** 1. **Design for future backend:** Structure data fetching and state management to be easily adapted to server-side later 2. **Implement smart caching:** Use browser localStorage for font metadata cache with TTL 3. **Optimize API usage:** Batch requests and use Google Fonts API efficiently 4. **Monitor usage:** Track API usage and user engagement to inform backend decision 5. **Migration path:** Keep server components clear of logic that could be moved to `+page.server.ts` later ### When to Add Backend Consider adding backend when: - ✅ User growth exceeds 1,000 daily active users - ✅ Analytics show users repeatedly saving comparisons manually - ✅ Feedback indicates desire for user accounts - ✅ Performance degrades due to repeated API calls - ✅ Planning to add premium features requiring payment processing --- ## Font Provider Integration Strategy ### Provider Overview | Provider | API Type | License | Font Count | Integration Complexity | |----------|----------|---------|------------|------------------------| | Google Fonts | REST API | Open Source (SIL/OFL) | 1,600+ | Low - Official API available | | Fontshare | GitHub/CDN | Free License | 100+ | Very Low - Static JSON available | --- ### Google Fonts Integration #### API Endpoint ``` https://www.googleapis.com/webfonts/v1/webfonts?key=YOUR_API_KEY ``` #### TypeScript Implementation ```typescript // src/lib/services/google-fonts.ts import type { FontMetadata, FontVariant, FontCategory } from '$lib/types'; export interface GoogleFontsResponse { kind: string; items: GoogleFontItem[]; } export interface GoogleFontItem { family: string; variants: string[]; subsets: string[]; version: string; lastModified: string; files: Record; category: 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace'; } export async function fetchGoogleFonts(apiKey: string): Promise { const response = await fetch( `https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}` ); if (!response.ok) { throw new Error(`Google Fonts API error: ${response.statusText}`); } const data: GoogleFontsResponse = await response.json(); return data.items.map(mapGoogleFontToMetadata); } function mapGoogleFontToMetadata(item: GoogleFontItem): FontMetadata { const category = mapGoogleCategory(item.category); return { id: `google-${item.family.toLowerCase().replace(/\s+/g, '-')}`, provider: 'google-fonts', family: item.family, displayName: item.family, category, variants: mapGoogleVariants(item.variants), license: { type: 'open-source', url: 'https://fonts.google.com/attribution', attribution: 'Google Fonts', commercial: true, embedding: 'webfont' }, languages: item.subsets, previewUrl: getGoogleFontPreviewUrl(item.family), cssUrl: getGoogleFontCssUrl(item.family, '400'), metadata: { version: item.version, lastModified: item.lastModified, files: item.files } }; } function mapGoogleCategory( category: GoogleFontItem['category'] ): FontCategory { const mapping = { 'sans-serif': 'sans-serif', 'serif': 'serif', 'display': 'display', 'handwriting': 'handwriting', 'monospace': 'monospace' } as const; return mapping[category] || 'display'; } function mapGoogleVariants(variants: string[]): FontVariant[] { return variants.map(v => { const weight = parseInt(v) || 400; const style = v.includes('italic') ? 'italic' : 'normal'; return { weight, style, name: v }; }); } function getGoogleFontPreviewUrl(family: string): string { return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@400&display=swap`; } function getGoogleFontCssUrl(family: string, weight: string): string { return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@${weight}&display=swap`; } ``` #### Dynamic Font Loading ```typescript // src/lib/utils/font-loader.ts let loadedFonts = new Set(); export async function loadFont(family: string, weight: number = 400): Promise { const fontKey = `${family}-${weight}`; if (loadedFonts.has(fontKey)) { return; } const cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@${weight}&display=swap`; try { // Create and inject link element const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = cssUrl; link.crossOrigin = 'anonymous'; await new Promise((resolve, reject) => { link.onload = () => { loadedFonts.add(fontKey); resolve(); }; link.onerror = () => reject(new Error(`Failed to load font: ${family}`)); document.head.appendChild(link); }); } catch (error) { console.error(`Failed to load font ${family}:`, error); throw error; } } export function unloadFont(family: string, weight: number = 400): void { const cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@${weight}&display=swap`; const links = document.querySelectorAll(`link[href="${cssUrl}"]`); links.forEach(link => link.remove()); loadedFonts.delete(`${family}-${weight}`); } export function preloadFonts(fonts: { family: string; weight: number }[]): void { fonts.forEach(({ family, weight }) => { const link = document.createElement('link'); link.rel = 'preload'; link.as = 'font'; link.type = 'font/woff2'; link.href = `https://fonts.gstatic.com/s/${family.toLowerCase()}/v${weight}`; link.crossOrigin = 'anonymous'; document.head.appendChild(link); }); } ``` --- ### Fontshare Integration #### Data Source Fontshare provides a static JSON file with all font metadata: ``` https://api.fontshare.com/v2/css?f[]= ``` Alternative: Use the GitHub repository data or scrape from the website. #### TypeScript Implementation ```typescript // src/lib/services/fontshare.ts import type { FontMetadata } from '$lib/types'; export interface FontshareMetadata { family: string; slug: string; styles: FontshareStyle[]; subsets: string[]; category: string; } export interface FontshareStyle { name: string; weight: number; file: string; unicodeRange?: string; } // Fontshare static font list (maintained manually or fetched from their docs) const FONTHARE_FONTS: FontshareMetadata[] = [ { family: 'Albert Sans', slug: 'albert-sans', styles: [ { name: 'Regular', weight: 400, file: 'https://api.fontshare.com/v2/css?f[]=albert-sans@400' }, { name: 'Medium', weight: 500, file: 'https://api.fontshare.com/v2/css?f[]=albert-sans@500' }, { name: 'Bold', weight: 700, file: 'https://api.fontshare.com/v2/css?f[]=albert-sans@700' } ], subsets: ['latin', 'latin-ext'], category: 'sans-serif' }, // Add more Fontshare fonts as needed... ]; export async function fetchFontshareFonts(): Promise { // In a real implementation, you might fetch this from a maintained JSON file // For MVP, we use the static list or fetch from Fontshare's API // Option 1: Use static list return FONTHARE_FONTS.map(mapFontshareToMetadata); // Option 2: Fetch from Fontshare API (if available) // const response = await fetch('https://api.fontshare.com/v2/fonts'); // const data = await response.json(); // return data.map(mapFontshareToMetadata); } function mapFontshareToMetadata(font: FontshareMetadata): FontMetadata { return { id: `fontshare-${font.slug}`, provider: 'fontshare', family: font.family, displayName: font.family, category: mapFontshareCategory(font.category), variants: font.styles.map(style => ({ weight: style.weight, style: 'normal', name: style.name, url: style.file })), license: { type: 'open-source', url: 'https://www.fontshare.com/license', attribution: 'Fontshare', commercial: true, embedding: 'webfont' }, languages: font.subsets, previewUrl: font.styles[0]?.file || '', cssUrl: font.styles[0]?.file || '', metadata: { slug: font.slug, styles: font.styles } }; } function mapFontshareCategory(category: string): FontCategory { const validCategories: FontCategory[] = ['sans-serif', 'serif', 'display', 'handwriting', 'monospace']; const normalized = category.toLowerCase().replace(' ', '-'); return validCategories.includes(normalized as FontCategory) ? (normalized as FontCategory) : 'display'; } ``` --- ### Data Normalization Both providers return different data structures. Normalize to a unified interface: ```typescript // src/lib/types/fonts.ts export type FontCategory = 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace'; export type FontProvider = 'google-fonts' | 'fontshare'; export interface LicenseInfo { type: 'open-source' | 'commercial' | 'free-for-personal'; url: string; attribution: string; commercial: boolean; embedding: 'webfont' | 'self-host' | 'both'; } export interface FontVariant { weight: number; style: 'normal' | 'italic'; name: string; url?: string; } export interface FontMetadata { id: string; provider: FontProvider; family: string; displayName: string; category: FontCategory; variants: FontVariant[]; license: LicenseInfo; languages: string[]; previewUrl: string; cssUrl: string; popularity?: number; // For sorting trending?: boolean; metadata: Record; } export interface ComparisonFont { font: FontMetadata; variant: FontVariant; size: number; lineHeight: number; letterSpacing: number; color: string; text?: string; // Override default preview text } export interface ComparisonSettings { text: string; backgroundColor: string; showGrid: boolean; watermark: boolean; } ``` --- ## Performance Optimization ### Lazy Loading Strategy #### Font Lazy Loading ```typescript // src/lib/hooks/useLazyFontLoader.ts import { tick } from 'svelte'; import { loadFont } from '$lib/utils/font-loader'; export function useLazyFontLoader() { let observer: IntersectionObserver | null = null; let elementsToLoad = new Map(); function observe( element: HTMLElement, family: string, weight: number = 400 ) { if (!observer) { observer = new IntersectionObserver( (entries) => { entries.forEach(async (entry) => { if (entry.isIntersecting) { const data = elementsToLoad.get(entry.target as HTMLElement); if (data) { await loadFont(data.family, data.weight); observer?.unobserve(entry.target); elementsToLoad.delete(entry.target as HTMLElement); } } }); }, { rootMargin: '200px', // Load before element enters viewport threshold: 0.01 } ); } elementsToLoad.set(element, { family, weight }); observer.observe(element); } function destroy() { observer?.disconnect(); observer = null; elementsToLoad.clear(); } return { observe, destroy }; } ``` #### Component-Level Usage ```svelte
{text}
``` ### Caching Strategy #### localStorage Caching with TTL ```typescript // src/lib/utils/cache.ts const CACHE_KEY = 'glyphdiff:font-cache'; const CACHE_VERSION = 1; const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours interface CacheEntry { version: number; timestamp: number; data: unknown; } export function setCache(key: string, data: T, ttl: number = CACHE_TTL): void { const entry: CacheEntry = { version: CACHE_VERSION, timestamp: Date.now(), data }; const cache = getCacheData(); cache[key] = entry; try { localStorage.setItem(CACHE_KEY, JSON.stringify(cache)); } catch (error) { console.warn('Failed to set cache:', error); } } export function getCache(key: string): T | null { const cache = getCacheData(); const entry = cache[key]; if (!entry) { return null; } // Check version if (entry.version !== CACHE_VERSION) { delete cache[key]; saveCacheData(cache); return null; } // Check TTL if (Date.now() - entry.timestamp > CACHE_TTL) { delete cache[key]; saveCacheData(cache); return null; } return entry.data as T; } export function clearCache(key?: string): void { if (key) { const cache = getCacheData(); delete cache[key]; saveCacheData(cache); } else { localStorage.removeItem(CACHE_KEY); } } function getCacheData(): Record { try { const cached = localStorage.getItem(CACHE_KEY); return cached ? JSON.parse(cached) : {}; } catch { return {}; } } function saveCacheData(data: Record): void { try { localStorage.setItem(CACHE_KEY, JSON.stringify(data)); } catch (error) { console.warn('Failed to save cache:', error); } } ``` #### Service Worker Caching (Optional for Enhanced PWA) ```typescript // src/lib/service-worker.ts const CACHE_NAME = 'glyphdiff-v1'; const FONT_CACHE_PREFIX = 'font-'; self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll([ '/', '/fonts', '/compare', // Add other static assets ]); }) ); }); self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); // Cache Google Fonts if (url.hostname === 'fonts.googleapis.com' || url.hostname === 'fonts.gstatic.com') { event.respondWith( caches.open(`${FONT_CACHE_PREFIX}${CACHE_NAME}`).then((cache) => { return cache.match(event.request).then((response) => { if (response) { return response; } return fetch(event.request).then((networkResponse) => { cache.put(event.request, networkResponse.clone()); return networkResponse; }); }); }) ); return; } // Network-first for API calls if (url.pathname.includes('/api/')) { event.respondWith( fetch(event.request) .then((response) => { const responseClone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, responseClone); }); return response; }) .catch(() => caches.match(event.request)) ); return; } // Cache-first for static assets event.respondWith( caches.match(event.request).then((response) => { return response || fetch(event.request); }) ); }); ``` ### Performance Metrics #### Target Core Web Vitals | Metric | Target | Why It Matters | |--------|--------|----------------| | LCP (Largest Contentful Paint) | < 2.5s | Main content loads quickly | | FID (First Input Delay) | < 100ms | Responsive to user input | | CLS (Cumulative Layout Shift) | < 0.1 | Visual stability | | TTFB (Time to First Byte) | < 600ms | Fast initial response | #### Performance Monitoring ```typescript // src/lib/utils/performance.ts export function reportWebVitals(): void { if (typeof window === 'undefined' || !('PerformanceObserver' in window)) { return; } // LCP const lcpObserver = new PerformanceObserver((list) => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1] as any; console.log('LCP:', lastEntry.startTime); // Send to analytics }); lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] }); // FID const fidObserver = new PerformanceObserver((list) => { const entries = list.getEntries(); const fid = entries[0] as any; console.log('FID:', fid.processingStart - fid.startTime); // Send to analytics }); fidObserver.observe({ entryTypes: ['first-input'] }); // CLS const clsObserver = new PerformanceObserver((list) => { let clsValue = 0; for (const entry of list.getEntries() as any[]) { if (!entry.hadRecentInput) { clsValue += entry.value; } } console.log('CLS:', clsValue); // Send to analytics }); clsObserver.observe({ entryTypes: ['layout-shift'] }); } ``` --- ## Frontend Architecture ### Project Structure ``` glyphdiff/ ├── src/ │ ├── lib/ │ │ ├── components/ │ │ │ ├── FontCard.svelte │ │ │ ├── FontPreview.svelte │ │ │ ├── ComparisonGrid.svelte │ │ │ ├── FilterBar.svelte │ │ │ └── DarkModeToggle.svelte │ │ ├── stores/ │ │ │ ├── fontStore.ts │ │ │ ├── comparisonStore.ts │ │ │ ├── filterStore.ts │ │ │ └── uiStore.ts │ │ ├── services/ │ │ │ ├── google-fonts.ts │ │ │ ├── fontshare.ts │ │ │ └── font-loader.ts │ │ ├── hooks/ │ │ │ ├── useLazyFontLoader.ts │ │ │ ├── useKeyboardShortcuts.ts │ │ │ └── useLocalStorage.ts │ │ ├── utils/ │ │ │ ├── cache.ts │ │ │ ├── format.ts │ │ │ └── url.ts │ │ ├── types/ │ │ │ ├── fonts.ts │ │ │ ├── comparison.ts │ │ │ └── index.ts │ │ └── constants.ts │ ├── routes/ │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ ├── +page.svelte # Home page │ │ ├── fonts/ │ │ │ ├── +page.svelte # Font catalog │ │ │ ├── +page.ts # Font data loading │ │ │ └── [slug]/ │ │ │ └── +page.svelte # Font detail page │ │ └── compare/ │ │ ├── +page.svelte # Comparison page │ │ └── +page.ts # URL state parsing │ ├── app.css │ └── app.d.ts ├── static/ │ ├── favicon.ico │ └── og-image.png ├── tests/ │ ├── unit/ │ └── e2e/ ├── package.json ├── svelte.config.js ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts ``` ### Component Architecture ``` ┌────────────────────────────────────────────────────────────┐ │ +layout.svelte │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Header │ │ │ │ Logo | Search | DarkModeToggle │ │ │ └──────────────────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Main │ │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ │ │ +page.svelte (Home) │ │ │ │ │ │ Hero section → Featured fonts → CTA │ │ │ │ │ └────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ │ │ fonts/+page.svelte │ │ │ │ │ │ FilterBar → FontGrid → FontCard │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ │ │FontCard │ │FontCard │ │FontCard │ ... │ │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ └────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ │ │ compare/+page.svelte │ │ │ │ │ │ ComparisonGrid → FontPreview × 4 │ │ │ │ │ │ Toolbar (text, size, color, settings) │ │ │ │ │ └────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Footer │ │ │ └──────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────┘ ``` ### Routing Strategy #### SvelteKit File-Based Routing | Route | File | Purpose | SSR/CSR | |-------|------|---------|---------| | `/` | `routes/+page.svelte` | Home/Landing page | SSR | | `/fonts` | `routes/fonts/+page.svelte` | Font catalog | SSG | | `/fonts/[slug]` | `routes/fonts/[slug]/+page.svelte` | Font details | SSG | | `/compare` | `routes/compare/+page.svelte` | Comparison tool | CSR | | `/api/...` | `routes/api/+server.ts` | Future API routes | Serverless | #### URL State for Comparisons ```typescript // src/routes/compare/+page.ts import type { PageLoad } from './$types'; export const load: PageLoad = ({ url }) => { const searchParams = url.searchParams; // Parse comparison state from URL const fontsParam = searchParams.get('fonts'); const text = searchParams.get('text') || 'The quick brown fox...'; const size = parseInt(searchParams.get('size') || '24'); const darkMode = searchParams.get('dark') === 'true'; let comparisonFonts: Array<{ family: string; weight: number }> = []; if (fontsParam) { try { comparisonFonts = JSON.parse(decodeURIComponent(fontsParam)); } catch (e) { console.error('Failed to parse fonts param:', e); } } return { initialFonts: comparisonFonts, settings: { text, size, darkMode } }; }; ``` ```svelte ``` --- ## Data Models ### Core Type Definitions ```typescript // src/lib/types/fonts.ts /** * Font provider sources */ export type FontProvider = 'google-fonts' | 'fontshare'; /** * Standard font categories */ export type FontCategory = | 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace'; /** * License information for fonts */ export interface LicenseInfo { /** License type classification */ type: 'open-source' | 'commercial' | 'free-for-personal'; /** URL to full license text */ url: string; /** Attribution required for display */ attribution: string; /** Commercial use permitted */ commercial: boolean; /** Allowed embedding methods */ embedding: 'webfont' | 'self-host' | 'both'; } /** * Individual font variant (weight/style combination) */ export interface FontVariant { /** Font weight (100-900) */ weight: number; /** Font style */ style: 'normal' | 'italic'; /** Display name for the variant */ name: string; /** URL to the font file (CSS or WOFF2) */ url?: string; /** Subset information (optional) */ subset?: string; } /** * Complete metadata for a font family */ export interface FontMetadata { /** Unique identifier for the font */ id: string; /** Source provider */ provider: FontProvider; /** Font family name (CSS property value) */ family: string; /** Display name for UI */ displayName: string; /** Font category */ category: FontCategory; /** Available variants */ variants: FontVariant[]; /** License information */ license: LicenseInfo; /** Supported language subsets */ languages: string[]; /** URL for font preview (CSS) */ previewUrl: string; /** URL for CSS import */ cssUrl: string; /** Popularity score (for sorting) */ popularity?: number; /** Trending status */ trending?: boolean; /** Additional provider-specific metadata */ metadata: Record; } /** * Font with selected variant for display */ export interface ActiveFont { /** Font metadata */ font: FontMetadata; /** Selected variant */ variant: FontVariant; } /** * Filter options for font catalog */ export interface FontFilters { /** Category filter */ categories: FontCategory[]; /** Weight range filter */ weightRange: [number, number]; /** Provider filter */ providers: FontProvider[]; /** Language subset filter */ languages: string[]; /** Search query */ search: string; /** Sort option */ sortBy: 'popularity' | 'name' | 'recent' | 'trending'; /** Sort direction */ sortDirection: 'asc' | 'desc'; } ``` ```typescript // src/lib/types/comparison.ts import type { FontMetadata, FontVariant } from './fonts'; /** * Display settings for comparison preview */ export interface FontDisplaySettings { /** Font size in pixels */ size: number; /** Line height (unitless multiplier) */ lineHeight: number; /** Letter spacing in pixels */ letterSpacing: number; /** Text color */ color: string; /** Custom preview text override */ text?: string; /** Text alignment */ textAlign: 'left' | 'center' | 'right'; } /** * Global comparison settings */ export interface ComparisonSettings { /** Default preview text */ text: string; /** Background color */ backgroundColor: string; /** Show comparison grid lines */ showGrid: boolean; /** Show watermarks/attribution */ watermark: boolean; /** Default display settings */ display: FontDisplaySettings; } /** * Font in a comparison session */ export interface ComparisonFont { /** Font metadata */ font: FontMetadata; /** Selected variant */ variant: FontVariant; /** Display settings (overrides global) */ settings: Partial; } /** * Complete comparison session */ export interface ComparisonSession { /** Unique session identifier */ id: string; /** Fonts being compared (max 4) */ fonts: ComparisonFont[]; /** Global comparison settings */ settings: ComparisonSettings; /** Session metadata */ metadata: { /** Creation timestamp */ createdAt: number; /** Last modified timestamp */ updatedAt: number; /** Session name */ name?: string; /** Whether session is saved */ saved: boolean; }; } /** * Shareable comparison data (for URL encoding) */ export interface ShareableComparison { /** Font identifiers and weights */ fonts: Array<{ family: string; weight: number }>; /** Preview text */ text?: string; /** Font size */ size?: number; /** Dark mode flag */ dark?: boolean; } ``` ```typescript // src/lib/types/ui.ts /** * UI theme options */ export type Theme = 'light' | 'dark' | 'system'; /** * UI settings persisted in localStorage */ export interface UISettings { /** Selected theme */ theme: Theme; /** Grid view density */ gridDensity: 'compact' | 'comfortable' | 'spacious'; /** Show font variants in catalog */ showVariants: boolean; /** Enable animations */ animationsEnabled: boolean; } /** * Toast notification types */ export type ToastType = 'success' | 'error' | 'info' | 'warning'; /** * Toast notification */ export interface Toast { id: string; type: ToastType; message: string; duration?: number; action?: { label: string; handler: () => void; }; } /** * Keyboard shortcut definition */ export interface KeyboardShortcut { key: string; ctrl?: boolean; shift?: boolean; alt?: boolean; description: string; action: () => void; } /** * Modal/Dialog state */ export interface ModalState { id: string | null; open: boolean; props?: Record; } ``` ```typescript // src/lib/types/index.ts // Re-export all types for convenient importing export * from './fonts'; export * from './comparison'; export * from './ui'; ``` --- ## Implementation Roadmap ### Week 1: Svelte Fundamentals **Goal:** Build foundational understanding of Svelte 5 reactive primitives and set up development environment. #### Tasks - [ ] **Project Setup** - [ ] Create SvelteKit project with TypeScript template - [ ] Install and configure Tailwind CSS 4.x - [ ] Install and set up Bits UI component library - [ ] Configure oxlint and dprint for linting and formatting - [ ] Set up Git repository and .gitignore - [ ] **Environment Configuration** - [ ] Create `.env.example` with GOOGLE_FONTS_API_KEY - [ ] Add `.env` to `.gitignore` - [ ] Configure environment variable loading in `vite.config.ts` - [ ] **Core Types Definition** - [ ] Create `src/lib/types/fonts.ts` - [ ] Create `src/lib/types/comparison.ts` - [ ] Create `src/lib/types/ui.ts` - [ ] Create `src/lib/types/index.ts` for re-exports - [ ] **Svelte 5 Reactive Primitives Practice** - [ ] Build Counter component using `$state` - [ ] Create DerivedCounter component using `$derived` - [ ] Implement EffectLogger component using `$effect` - [ ] Practice with reactive arrays and objects - [ ] **Practice Components** - [ ] Build simple Button component - [ ] Create Toggle component - [ ] Implement Select dropdown component - [ ] Build simple FilterBar component #### Learning Outcomes - Understand Svelte 5's new runes system (`$state`, `$derived`, `$effect`) - Comfortable with Svelte component structure and syntax - Familiar with Bits UI components - Tailwind CSS integration working #### Week 1 Deliverables - Working development environment - 4 practice components demonstrating Svelte reactivity - Core TypeScript types defined --- ### Week 2: SvelteKit & Routing **Goal:** Build font catalog page with routing, data fetching, and understand SvelteKit's SSR capabilities. #### Tasks - [ ] **Google Fonts Service** - [ ] Create `fetchGoogleFonts()` function - [ ] Implement `mapGoogleFontToMetadata()` mapper - [ ] Add TypeScript interfaces for Google Fonts API response - [ ] Test with Google Fonts API key - [ ] **Fontshare Service** - [ ] Create `fetchFontshareFonts()` function - [ ] Build static font list for Fontshare - [ ] Implement `mapFontshareToMetadata()` mapper - [ ] Combine both providers into single font catalog - [ ] **Font Catalog Page** - [ ] Create `routes/fonts/+page.svelte` - [ ] Implement `routes/fonts/+page.ts` for data loading - [ ] Build FontCard component - [ ] Create responsive FontGrid layout - [ ] **Font Detail Page** - [ ] Create `routes/fonts/[slug]/+page.svelte` - [ ] Implement dynamic route parameter extraction - [ ] Build font detail view with all variants - [ ] Add "Add to Comparison" button - [ ] **Comparison Page Skeleton** - [ ] Create `routes/compare/+page.svelte` - [ ] Implement URL state parsing in `+page.ts` - [ ] Build basic layout for comparison grid - [ ] **SSR Understanding** - [ ] Explore SvelteKit rendering modes (SSR, CSR, SSG) - [ ] Test page performance with SSR enabled - [ ] Understand hydration process - [ ] Configure static generation for font catalog #### Learning Outcomes - Understand SvelteKit file-based routing - Learn data loading patterns (`+page.ts`, `+page.server.ts`) - Grasp SSR vs CSR concepts and trade-offs - Comfortable with dynamic routes and parameters #### Week 2 Deliverables - Working font catalog page with Google Fonts data - Font detail pages with all metadata - Basic comparison page with URL state - Understanding of SvelteKit rendering modes --- ### Week 3: State Management **Goal:** Implement comprehensive state management with Svelte stores, localStorage persistence, and URL state synchronization. #### Tasks - [ ] **Font Store** - [ ] Create `src/lib/stores/fontStore.ts` - [ ] Implement `$state` for fonts array - [ ] Add derived state for filtered fonts - [ ] Implement localStorage persistence - [ ] Add loading and error states - [ ] **Filter Store** - [ ] Create `src/lib/stores/filterStore.ts` - [ ] Implement `$state` for all filter options - [ ] Add derived state for active filter count - [ ] Implement reset filters function - [ ] Add localStorage persistence - [ ] **Comparison Store** - [ ] Create `src/lib/stores/comparisonStore.ts` - [ ] Implement `$state` for comparison fonts (max 4) - [ ] Add functions to add/remove fonts - [ ] Implement comparison settings state - [ ] Add shareable URL generation - [ ] Implement localStorage persistence - [ ] **UI Store** - [ ] Create `src/lib/stores/uiStore.ts` - [ ] Implement theme toggle logic - [ ] Add settings for grid density, animations - [ ] Implement system theme detection - [ ] Add localStorage persistence - [ ] **Cache Utility** - [ ] Implement `cache.ts` with TTL support - [ ] Add caching to Google Fonts fetcher - [ ] Implement cache invalidation logic - [ ] Add cache management UI (optional) - [ ] **URL State Integration** - [ ] Sync comparison state with URL - [ ] Sync filter state with URL (optional) - [ ] Implement deep linking to comparisons - [ ] Handle URL encoding/decoding #### Learning Outcomes - Master Svelte 5 stores with `$state` runes - Understand localStorage persistence patterns - Learn URL state synchronization - Practice derived state and computed values #### Week 3 Deliverables - Fully functional stores for fonts, filters, comparisons, UI - localStorage persistence working - URL state synchronization working - Caching layer implemented --- ### Week 4: UI & Polish **Goal:** Build polished UI with Bits UI components, dark mode, responsive design, and smooth animations. #### Tasks - [ ] **Layout Components** - [ ] Build Header component with logo and navigation - [ ] Create Footer component with links and attribution - [ ] Implement global layout in `+layout.svelte` - [ ] Add mobile navigation menu - [ ] **FilterBar Component** - [ ] Use Bits UI Select for category filter - [ ] Use Bits UI Dialog for advanced filters - [ ] Add search input with debouncing - [ ] Implement active filter indicators - [ ] **ComparisonGrid Component** - [ ] Use Bits UI Grid for responsive layout - [ ] Implement drag-and-drop reordering (optional) - [ ] Add font settings panel per column - [ ] Implement "Remove font" button - [ ] **FontPreview Component** - [ ] Build preview text area - [ ] Add size, weight, line-height controls - [ ] Implement color picker - [ ] Add lazy loading for fonts - [ ] **Dark Mode** - [ ] Implement dark mode toggle - [ ] Configure Tailwind dark mode classes - [ ] Add system preference detection - [ ] Test color contrast in both themes - [ ] **Responsive Design** - [ ] Test on mobile (375px) - [ ] Test on tablet (768px) - [ ] Test on desktop (1280px+) - [ ] Adjust grid columns for breakpoints - [ ] **Animations & Transitions** - [ ] Add page transitions using Svelte transitions - [ ] Implement loading skeletons - [ ] Add hover effects on FontCard - [ ] Create toast notification system - [ ] **Accessibility** - [ ] Add ARIA labels to interactive elements - [ ] Ensure keyboard navigation works - [ ] Test with screen reader - [ ] Verify color contrast ratios - [ ] **Performance Optimization** - [ ] Implement image/font lazy loading - [ ] Add code splitting where appropriate - [ ] Test Lighthouse scores - [ ] Optimize bundle size #### Learning Outcomes - Proficient with Bits UI component library - Comfortable building responsive layouts - Understand accessibility best practices - Experience with performance optimization #### Week 4 Deliverables - Polished, production-ready UI - Dark mode fully implemented - Responsive design across all breakpoints - Accessibility improvements completed - Performance targets met --- ## TanStack Query Research ### What is TanStack Query? TanStack Query (formerly React Query) is a powerful data synchronization library for fetching, caching, and managing asynchronous state. It's widely used in the React ecosystem. ### Key Features - **Automatic caching and refetching** - **Loading and error state management** - **Optimistic updates** - **Pagination support** - **Request deduplication** ### TanStack Query for Svelte? TanStack Query has a Svelte implementation (`@tanstack/svelte-query`) that provides similar functionality to the React version. ### Why TanStack Query is NOT Recommended for MVP | Factor | TanStack Query | SvelteKit Load | Recommendation | |--------|----------------|----------------|----------------| | **Data Source** | Client-side API calls | Server-side data loading | SvelteKit Load is sufficient | | **Complexity** | Additional dependency | Built-in to framework | SvelteKit reduces complexity | | **SSR Support** | Requires hydration | Native SSR | SvelteKit is cleaner | | **Learning Curve** | Additional concepts to learn | Learn SvelteKit once | Focus on SvelteKit | | **Bundle Size** | ~13KB gzipped | 0KB (built-in) | Smaller bundle | | **Type Safety** | Good | Excellent | SvelteKit with TS is robust | | **Caching Strategy** | In-memory cache | Static generation + CDN | SSG is more performant | ### Use Cases Where TanStack Query Would Be Useful Consider adding TanStack Query in the future if: - Implementing user authentication and profile data - Building real-time features (WebSocket, polling) - Complex pagination with infinite scroll - Multiple API endpoints requiring complex cache invalidation - Offline-first functionality ### Recommended Data Fetching Approach for MVP #### Use SvelteKit's Data Loading ```typescript // ✅ Recommended: SvelteKit load function // src/routes/fonts/+page.ts import { fetchGoogleFonts, fetchFontshareFonts } from '$lib/services'; import { setCache, getCache } from '$lib/utils/cache'; export const load = async ({ fetch, depends }) => { // Check cache first const cached = getCache('font-catalog'); if (cached) { return { fonts: cached, cached: true }; } // Fetch from providers const [googleFonts, fontshareFonts] = await Promise.all([ fetchGoogleFonts(import.meta.env.VITE_GOOGLE_FONTS_API_KEY), fetchFontshareFonts() ]); const allFonts = [...googleFonts, ...fontshareFonts]; // Cache for 24 hours setCache('font-catalog', allFonts); // Invalidate cache when needed depends('app:fonts'); return { fonts: allFonts, cached: false }; }; ``` #### Use Svelte Stores for Client-Side State ```typescript // ✅ Recommended: Svelte 5 stores with $state // src/lib/stores/fontStore.ts import { setCache, getCache } from '$lib/utils/cache'; class FontStore { fonts = $state([]); loading = $state(false); error = $state(null); async loadFonts(apiKey: string) { this.loading = true; this.error = null; try { const cached = getCache('font-catalog'); if (cached) { this.fonts = cached; return; } const [googleFonts, fontshareFonts] = await Promise.all([ fetchGoogleFonts(apiKey), fetchFontshareFonts() ]); this.fonts = [...googleFonts, ...fontshareFonts]; setCache('font-catalog', this.fonts); } catch (e) { this.error = e as Error; } finally { this.loading = false; } } } export const fontStore = new FontStore(); ``` ### Conclusion: Do NOT Use TanStack Query for MVP **Reasoning:** 1. SvelteKit's data loading capabilities are sufficient for this project's needs 2. Focus on learning Svelte 5 and SvelteKit rather than adding more dependencies 3. Static generation provides better performance than client-side caching 4. Simpler codebase is easier to maintain while learning **Future Consideration:** Revisit this decision if the project grows to include: - User authentication - Real-time features - Complex client-side data synchronization --- ## Learning Guide: React → Svelte ### React vs Svelte 5 Comparison | React Concept | Svelte 5 Equivalent | Notes | |--------------|---------------------|-------| | `useState` | `$state` | No setter function needed, direct reassignment triggers reactivity | | `useEffect` | `$effect` | Auto-tracks dependencies, no dependency array needed | | `useMemo` | `$derived` | Computed values that automatically update | | `useCallback` | Direct function | Functions are auto-memoized in Svelte 5 | | `children` prop | `` | Slot composition with named slots | | `props` | `export let` | Props declared with `export let propName` | | `context` | `setContext` / `getContext` | Similar but with cleaner API | | `useRef` | `bind:this` | Direct element binding instead of ref objects | | `useState(prev => prev + 1)` | `count += 1` | Direct mutation works with $state | | `useReducer` | Custom store with methods | Svelte stores are more flexible | | `forwardRef` | N/A | Ref forwarding handled automatically | | `useImperativeHandle` | N/A | Call methods directly on component instances | ### Code Examples #### Counter Component **React:** ```tsx import { useState, useEffect } from 'react'; function Counter({ initial = 0 }: { initial?: number }) { const [count, setCount] = useState(initial); const [doubled, setDoubled] = useState(0); useEffect(() => { setDoubled(count * 2); }, [count]); return (

Count: {count}

Doubled: {doubled}

); } ``` **Svelte 5:** ```svelte

Count: {count}

Doubled: {doubled}

``` **Key Differences:** - No setter function needed - direct mutation works with `$state` - `$derived` eliminates need for `useEffect` with dependencies - Functions are defined separately (no need for `useCallback`) - Event handlers use lowercase `onclick` instead of `onClick` --- #### Data Fetching **React with React Query:** ```tsx import { useQuery } from '@tanstack/react-query'; function FontList() { const { data: fonts, isLoading, error } = useQuery({ queryKey: ['fonts'], queryFn: async () => { const response = await fetch('/api/fonts'); if (!response.ok) throw new Error('Failed to fetch'); return response.json(); } }); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; return (
    {fonts.map((font: any) => (
  • {font.displayName}
  • ))}
); } ``` **SvelteKit with load function:** ```typescript // +page.ts export const load = async ({ fetch }) => { const response = await fetch('https://www.googleapis.com/webfonts/v1/webfonts?key=' + import.meta.env.VITE_GOOGLE_FONTS_API_KEY); const fonts = await response.json(); return { fonts }; }; ``` ```svelte {#if data.fonts}
    {#each data.fonts as font}
  • {font.family}
  • {/each}
{/if} ``` **Key Differences:** - SvelteKit loads data on the server (SSR) by default - No loading state needed for initial page load - Data flows from `load` function to page component via props --- #### State Management **React with Zustand:** ```ts // store.ts import { create } from 'zustand'; interface FontStore { fonts: FontMetadata[]; selectedFont: string | null; setFonts: (fonts: FontMetadata[]) => void; selectFont: (id: string) => void; } export const useFontStore = create((set) => ({ fonts: [], selectedFont: null, setFonts: (fonts) => set({ fonts }), selectFont: (id) => set({ selectedFont: id }) })); ``` ```tsx // Component.tsx import { useFontStore } from './store'; function FontSelector() { const { fonts, selectedFont, selectFont } = useFontStore(); return ( ); } ``` **Svelte with Store:** ```ts // fontStore.ts import type { FontMetadata } from '$lib/types'; class FontStore { fonts = $state([]); selectedFont = $state(null); setFonts(fonts: FontMetadata[]) { this.fonts = fonts; } selectFont(id: string) { this.selectedFont = id; } } export const fontStore = new FontStore(); ``` ```svelte ``` **Key Differences:** - Svelte stores use `$state` directly, no need for immer - Two-way binding with `bind:value` instead of `onChange` - Store access is simpler - just import and use --- #### Conditional Rendering **React:** ```tsx function ConditionalRender({ fonts, loading }: { fonts: Font[]; loading: boolean }) { if (loading) { return ; } if (fonts.length === 0) { return ; } return (
{fonts.map(font => ( ))}
); } ``` **Svelte:** ```svelte {#if loading} {:else if fonts.length === 0} {:else}
{#each fonts as font} {/each}
{/if} ``` **Key Differences:** - Svelte uses `{#if}`, `{:else if}`, `{:else}`, `{/if}` block syntax - No ternary operator needed inside JSX - `{#each}` block with `{/each}` for lists --- #### Component Props **React:** ```tsx interface FontCardProps { font: FontMetadata; onClick?: (font: FontMetadata) => void; size?: number; showLicense?: boolean; } function FontCard({ font, onClick, size = 16, showLicense = false }: FontCardProps) { return (

{font.displayName}

{showLicense && } {onClick && }
); } ``` **Svelte:** ```svelte

{font.displayName}

{#if showLicense} {/if} {#if onClick} {/if}
``` **Key Differences:** - Props declared with `export let` instead of function parameters - Default values assigned with `=` in declaration - Event handlers use lowercase (`onclick`) - Props passed as attributes without curly braces for simple values --- #### Slot/Children Composition **React:** ```tsx // Modal.tsx function Modal({ children, title, footer }: { children: React.ReactNode; title: string; footer?: React.ReactNode; }) { return (
{title}
{children}
{footer &&
{footer}
}
); } // Usage Save}>

Choose a font from the list below.

``` **Svelte:** ```svelte

Choose a font from the list below.

``` **Key Differences:** - Named slots using `slot="name"` and `` - Multiple slots with different names - Default slot uses `` without name - `` for content without wrapper element --- ### Common Patterns #### Form Handling **React:** ```tsx function SearchForm({ onSearch }: { onSearch: (query: string) => void }) { const [query, setQuery] = useState(''); const handleSubmit = (e: FormEvent) => { e.preventDefault(); onSearch(query); }; return (
setQuery(e.target.value)} placeholder="Search fonts..." />
); } ``` **Svelte:** ```svelte
``` **Key Difference:** Two-way binding with `bind:value` eliminates the need for `onChange`. --- #### Lists with Keys **React:** ```tsx {fonts.map(font => ( handleSelect(font)} /> ))} ``` **Svelte:** ```svelte {#each fonts as font (font.id)} handleSelect(font)} /> {/each} ``` **Key Difference:** Key specified in parentheses after item name: `as item (key)`. --- #### Lifecycle Methods **React:** ```tsx useEffect(() => { console.log('Mounted'); return () => console.log('Cleanup'); }, []); useEffect(() => { console.log('count changed:', count); }, [count]); ``` **Svelte:** ```svelte ``` --- ## Scalability & Commercialization ### When to Add Backend #### Indicators for Backend Implementation | Metric | Threshold | Action | |--------|-----------|--------| | Daily Active Users | > 1,000 | Consider backend | | Google Fonts API Usage | > 800/day (80% of limit) | Implement server proxy | | Feature Requests | > 10/month for user accounts | Plan authentication | | Comparison Saves | Users frequently export/save | Add database | | Page Load Time | > 3s on mobile | Optimize with backend caching | #### Backend Architecture (When Needed) ``` ┌─────────────────────────────────────────────────────────────┐ │ Vercel Edge │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ SvelteKit Server Functions │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ API Routes │ │ Auth Routes │ │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ │ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ Vercel Postgres │ │ Vercel KV │ │ (User accounts, │ │ (Caching, │ │ saved comps) │ │ rate limiting) │ └──────────────────┘ └──────────────────┘ ``` #### Recommended Backend Stack | Component | Technology | Why | |-----------|------------|-----| | Server Runtime | Vercel Edge Functions | Fast, global, free tier generous | | Database | Vercel Postgres (Neon) | Managed, edge-ready, TypeScript native | | Caching | Vercel KV (Redis) | Fast key-value store, built-in | | Authentication | Lucia Auth | Lightweight, framework-agnostic | | ORM | Drizzle ORM | TypeScript-first, no runtime overhead | | File Storage | Vercel Blob | Simple, scalable storage | --- ### Monetization Models #### Tier 1: Freemium (Recommended Launch Model) | Feature | Free | Pro ($5/mo) | |---------|------|-------------| | Font comparisons | 4 fonts max | Unlimited | | Save comparisons | Browser storage only | Cloud sync | | Export images | Watermarked | No watermark | | Custom text | Limited chars | Unlimited | | Advanced filters | Basic | Advanced | | API access | ❌ | ✅ | #### Tier 2: Team/Agency ($29/mo) - All Pro features - Team collaboration (shared workspaces) - Brand management (save brand fonts) - Custom domains for shared comparisons - Priority support - Monthly usage reports #### Tier 3: Enterprise (Custom) - Unlimited API access - Self-hosted deployment option - Custom integrations - SSO authentication - Dedicated support - SLA guarantees --- ### Additional Features for Monetization 1. **Font Pairing Recommendations** - ML-based font matching - Designer-curated pairings - Upload custom fonts for analysis 2. **CSS Generator** - Export optimized CSS - Variable font support - Fallback font stacks 3. **Embeddable Widget** - Embed comparison on external sites - White-label options - Custom branding 4. **Font Inspector** - Detailed glyph analysis - Character set viewer - Font metrics display 5. **A/B Testing** - Test font combinations - User feedback collection - Conversion tracking --- ## Deployment ### Vercel Configuration #### vercel.json ```json { "buildCommand": "yarn build", "devCommand": "yarn dev", "installCommand": "yarn install", "framework": "sveltekit", "regions": ["iad1"], "headers": [ { "source": "/(.*)", "headers": [ { "key": "X-Content-Type-Options", "value": "nosniff" }, { "key": "X-Frame-Options", "value": "DENY" }, { "key": "X-XSS-Protection", "value": "1; mode=block" }, { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" } ] }, { "source": "/static/(.*)", "headers": [ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } ] }, { "source": "/fonts/(.*)", "headers": [ { "key": "Cache-Control", "value": "public, max-age=604800" } ] } ], "rewrites": [ { "source": "/api/:path*", "destination": "/api/:path*" } ] } ``` --- ### Environment Configuration #### .env.example ```bash # Google Fonts API Key # Get from: https://developers.google.com/fonts/docs/developer_api GOOGLE_FONTS_API_KEY=your_api_key_here # Application Configuration APP_URL=https://glyphdiff.com APP_NAME=GlyphDiff # Analytics (Optional) VITE_GA_ID=G-XXXXXXXXXX VITE_PLAUSIBLE_DOMAIN= glyphdiff.com # Feature Flags VITE_ENABLE_CACHE=true VITE_ENABLE_ANALYTICS=false ``` #### vite.config.ts ```typescript import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], define: { // Expose env variables to client code 'import.meta.env.VITE_GOOGLE_FONTS_API_KEY': JSON.stringify(process.env.GOOGLE_FONTS_API_KEY), } }); ``` --- ### DNS Configuration #### DNS Records for glyphdiff.com | Type | Name | Value | TTL | |------|------|-------|-----| | A | @ | 76.76.21.21 | 3600 | | CNAME | www | cname.vercel-dns.com | 3600 | **Note:** 76.76.21.21 is Vercel's IPv4 address for custom domains. --- ### Build and Deploy Commands ```bash # Install dependencies yarn install # Run development server yarn dev # Build for production yarn build # Preview production build yarn preview # Run tests yarn test # Run linter yarn lint # Format code yarn format # Type check yarn check ``` #### package.json Scripts ```json { "name": "glyphdiff", "version": "0.1.0", "private": true, "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "oxlint", "format": "dprint fmt", "format:check": "dprint check", "test": "playwright test", "test:unit": "vitest" }, "devDependencies": { "@sveltejs/adapter-vercel": "^5.0.0", "@sveltejs/kit": "^2.0.0", "svelte": "^5.0.0", "typescript": "^5.3.0", "vite": "^5.0.0" } } ``` --- ### Deployment Steps 1. **Push to GitHub** ```bash git init git add . git commit -m "Initial commit" git branch -M main git remote add origin git@github.com:your-username/glyphdiff.git git push -u origin main ``` 2. **Connect to Vercel** - Go to [vercel.com](https://vercel.com) - Click "Add New Project" - Import from GitHub - Configure environment variables: - `GOOGLE_FONTS_API_KEY` 3. **Configure Custom Domain** - Go to Project Settings → Domains - Add `glyphdiff.com` - Add `www.glyphdiff.com` - Update DNS records as shown above 4. **Deploy** - Push changes to GitHub - Vercel automatically deploys on push to main - Preview deployments for pull requests --- ## Risk Assessment ### Technical Risks | Risk | Probability | Impact | Mitigation | |------|-------------|--------|------------| | Google Fonts API rate limiting | Medium | High | Implement caching, batch requests, consider backend proxy | | Browser font loading issues | Low | Medium | Use `document.fonts.load()`, implement fallback fonts | | Poor mobile performance | Medium | High | Lazy loading, code splitting, test on real devices | | Accessibility violations | Low | Medium | Regular audits with axe DevTools, keyboard testing | | Vercel free tier limits | Low | Low | Monitor usage, easy upgrade path | ### Learning Curve Risks | Risk | Probability | Impact | Mitigation | |------|-------------|--------|------------| | Difficulty with Svelte 5 runes | Medium | High | Start with simple components, practice with $state basics | | TypeScript integration issues | Low | Medium | Use strict mode from start, leverage SvelteKit templates | | CSS/Tailwind complexity | Low | Low | Use utility classes, avoid custom CSS where possible | | Bits UI learning curve | Low | Medium | Read documentation, check examples, copy patterns | ### External Dependency Risks | Risk | Probability | Impact | Mitigation | |------|-------------|--------|------------| | Google Fonts API changes | Low | High | Use TypeScript interfaces, watch for deprecation notices | | Fontshare availability changes | Low | Low | Include as optional source, can be removed if needed | | Vercel downtime | Very Low | Medium | Static files served from CDN, minimal impact | | Bits UI breaking changes | Medium | Low | Lock to specific version, update carefully | --- ## Success Metrics ### Learning Success Metrics | Metric | Target | Measurement | |--------|--------|-------------| | Svelte 5 proficiency | Comfortable with $state, $derived, $effect | Self-assessment quiz | | SvelteKit understanding | Build 3+ routes with data loading | Route count | | Store management | Implement 4 stores with persistence | Store count | | Component library usage | Use 5+ Bits UI components | Component count | | TypeScript adoption | 100% typed codebase | Lint check | ### Project Success Metrics | Metric | Target | Measurement | |--------|--------|-------------| | Functional font catalog | 1000+ fonts loaded | Google Fonts API response | | Comparison tool working | 4 fonts simultaneously | Manual testing | | Performance score | 90+ Lighthouse score | Lighthouse audit | | Mobile responsiveness | Pass on 375px - 1280px | Browser DevTools | | Accessibility | WCAG 2.1 AA compliant | axe DevTools | ### Milestones - [ ] **Week 1 Complete:** Environment setup, basic components, types defined - [ ] **Week 2 Complete:** Font catalog with data fetching, routing working - [ ] **Week 3 Complete:** All stores implemented, URL state working - [ ] **Week 4 Complete:** Polished UI, dark mode, responsive design - [ ] **MVP Launch:** Application deployed to glyphdiff.com - [ ] **Post-Launch:** Collect feedback, iterate on features --- ## Appendix ### Commands Reference #### Project Initialization ```bash # Create new SvelteKit project yarn create svelte@latest glyphdiff # Select options: # - Which Svelte app template? Skeleton project # - Add type checking? Yes, using TypeScript # - Select additional options: Playwright # Navigate to project cd glyphdiff # Install dependencies yarn install # Add Tailwind CSS yarn dlx svelte-add@latest tailwindcss # Add Bits UI yarn add bits-ui # Install additional dependencies yarn add clsx tailwind-merge # Install oxlint and dprint yarn add -D oxlint dprint ``` #### Development Commands ```bash # Start development server yarn dev # Open browser to localhost:5173 # Type check yarn check # Type check in watch mode yarn check:watch # Lint code yarn lint # Format code yarn format # Run Playwright tests yarn test # Run unit tests (Vitest) yarn test:unit ``` #### Build Commands ```bash # Build for production yarn build # Preview production build yarn preview # Clean build artifacts rm -rf .svelte-kit build ``` #### Git Commands ```bash # Initialize git repo git init # Create .gitignore echo ".svelte-kit node_modules .env .DS_Store dist build .vercel" > .gitignore # Stage all files git add . # Commit git commit -m "Initial commit" # Create main branch git branch -M main # Add remote git remote add origin git@github.com:username/glyphdiff.git # Push to remote git push -u origin main ``` --- ### Learning Resources #### Official Documentation - [Svelte 5 Documentation](https://svelte-5-preview.vercel.app/) - [SvelteKit Documentation](https://kit.svelte.dev/docs) - [Tailwind CSS Documentation](https://tailwindcss.com/docs) - [Bits UI Documentation](https://www.bits-ui.com/) #### React to Svelte Migration - [Svelte for React Developers](https://svelte.dev/docs/svelte-vs-react) - [Svelte 5 Runes Guide](https://svelte-5-preview.vercel.app/docs/runes-api) - [SvelteKit Data Loading](https://kit.svelte.dev/docs/load) #### Google Fonts API - [Google Fonts API Developer Guide](https://developers.google.com/fonts/docs/developer_api) - [Google Fonts API Reference](https://developers.google.com/fonts/docs/reference/rest) #### Video Resources - [Svelte 5 Tutorial](https://www.youtube.com/results?search_query=svelte+5+tutorial) - [SvelteKit Crash Course](https://www.youtube.com/results?search_query=sveltekit+crash+course) - [Tailwind CSS Full Course](https://www.youtube.com/results?search_query=tailwind+css+full+course) #### Community - [Svelte Discord](https://svelte.dev/chat) - [Svelte subreddit](https://reddit.com/r/sveltejs) - [SvelteKit subreddit](https://reddit.com/r/sveltekit) --- ### File Templates #### Component Template ```svelte

{prop}

Count: {count}, Doubled: {doubled}

``` #### Store Template ```typescript import type { FontMetadata } from '$lib/types'; import { setCache, getCache } from '$lib/utils/cache'; class ExampleStore { // State items = $state([]); loading = $state(false); error = $state(null); // Derived state (computed automatically) get count() { return this.items.length; } // Methods async loadItems() { this.loading = true; this.error = null; try { // Load from cache first const cached = getCache('items'); if (cached) { this.items = cached; return; } // Fetch items // this.items = await fetchItems(); // setCache('items', this.items); } catch (e) { this.error = e as Error; } finally { this.loading = false; } } addItem(item: FontMetadata) { this.items = [...this.items, item]; } removeItem(id: string) { this.items = this.items.filter(item => item.id !== id); } } // Export singleton instance export const exampleStore = new ExampleStore(); ``` #### Load Function Template ```typescript // +page.ts or +page.server.ts import type { PageLoad } from './$types'; export const load: PageLoad = async ({ fetch, depends, url }) => { // Declare dependencies for invalidation depends('app:data'); // Get URL parameters const search = url.searchParams.get('search') || ''; try { // Fetch data const response = await fetch('/api/data'); const data = await response.json(); return { data, search }; } catch (error) { return { data: [], search, error: error.message }; } }; ``` --- ### Troubleshooting #### Common Issues **Issue: Google Fonts API returns 403** ```bash # Solution: Check API key and rate limits # 1. Verify API key is correct # 2. Check API key restrictions (set to None for testing) # 3. Verify you haven't exceeded daily quota ``` **Issue: Tailwind classes not working** ```bash # Solution: Check configuration # 1. Ensure app.css imports tailwind directives # 2. Restart dev server after config changes # 3. Check vite.config.ts includes postcss config ``` **Issue: Svelte 5 syntax errors** ```bash # Solution: Ensure you're using latest Svelte npm install svelte@next # If using VS Code, install Svelte extension # Ensure it's the latest version for Svelte 5 support ``` **Issue: TypeScript errors with $state** ```bash # Solution: Update Svelte configuration # Ensure svelte.config.js includes: export default { compilerOptions: { runes: true } }; ``` **Issue: Vercel deployment fails** ```bash # Solution: Check build output npm run build # Review error logs in Vercel dashboard # Common issues: # - Missing environment variables # - TypeScript errors # - Build timeout ``` --- ### Performance Checklist - [ ] Implement lazy loading for fonts - [ ] Use SvelteKit static generation where possible - [ ] Optimize images (use webp format) - [ ] Minimize JavaScript bundle size - [ ] Use code splitting for routes - [ ] Implement caching strategy - [ ] Enable compression (handled by Vercel) - [ ] Test Lighthouse scores - [ ] Minimize layout shifts - [ ] Use appropriate font formats (WOFF2) --- ## Conclusion This project plan provides a comprehensive roadmap for building glyphdiff.com as a learning-focused MVP. The pure client-side approach prioritizes mastering Svelte 5 and SvelteKit while building a functional font comparison tool. ### Next Steps 1. Set up the development environment (Week 1 tasks) 2. Obtain Google Fonts API key 3. Configure environment variables 4. Begin implementing Svelte fundamentals Good luck on your React → Svelte journey! 🚀 --- *Document Version: 1.0* *Last Updated: December 2025* *Project: glyphdiff.com*