diff --git a/app/api/web-vitals/route.ts b/app/api/web-vitals/route.ts index 468ce50..a1beafe 100644 --- a/app/api/web-vitals/route.ts +++ b/app/api/web-vitals/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import fs from "fs"; import path from "path"; +import { logger } from "../../../lib/logger"; const WEB_VITALS_DIR = path.join(process.cwd(), ".next", "web-vitals"); @@ -65,7 +66,7 @@ export async function POST(request: NextRequest) { existingData = JSON.parse(fileContent) as WebVitalData[]; } catch (error) { const err = error as Error; - console.warn("Could not parse existing vitals data:", err.message); + logger.warn("Could not parse existing vitals data:", err.message); } } @@ -79,13 +80,13 @@ export async function POST(request: NextRequest) { fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2)); // Log for monitoring - console.log( + logger.info( `Web Vital received: ${metric} = ${data.value}ms (${data.rating})`, ); return NextResponse.json({ success: true }); } catch (error) { - console.error("Error processing web vital:", error); + logger.error("Error processing web vital:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, @@ -141,7 +142,7 @@ export async function GET() { return NextResponse.json({ metrics }); } catch (error) { - console.error("Error fetching web vitals:", error); + logger.error("Error fetching web vitals:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index ad8a368..ca857d7 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -6,6 +6,7 @@ import { getAllBlogPosts as getAllPosts, type BlogPost, } from "../../../lib/content"; +import { logger } from "../../../lib/logger"; import ContentBanner from "../../components/ContentBanner"; import AskOrganizer from "../../components/AskOrganizer"; import { getAssetPath, ASSETS } from "../../../lib/assetUtils"; @@ -44,7 +45,7 @@ export async function generateStaticParams() { slug: post.slug, })); } catch (error) { - console.error("Error generating static params:", error); + logger.error("Error generating static params:", error); return []; } } @@ -87,7 +88,7 @@ export async function generateMetadata({ }, }; } catch (error) { - console.error("Error generating metadata:", error); + logger.error("Error generating metadata:", error); return { title: "Blog Post", description: "A blog post from our community.", diff --git a/app/components/ErrorBoundary.tsx b/app/components/ErrorBoundary.tsx index 915a6a1..78896f9 100644 --- a/app/components/ErrorBoundary.tsx +++ b/app/components/ErrorBoundary.tsx @@ -1,6 +1,7 @@ "use client"; import React, { Component, type ReactNode } from "react"; +import { logger } from "../../lib/logger"; interface ErrorBoundaryProps { children: ReactNode; @@ -24,7 +25,7 @@ class ErrorBoundary extends Component { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { // Log the error to an error reporting service - console.error("ErrorBoundary caught an error:", error, errorInfo); + logger.error("ErrorBoundary caught an error:", error, errorInfo); } render() { diff --git a/app/components/QuoteBlock.tsx b/app/components/QuoteBlock.tsx index 79f7bcd..1eb1619 100644 --- a/app/components/QuoteBlock.tsx +++ b/app/components/QuoteBlock.tsx @@ -3,6 +3,7 @@ import { useState, memo } from "react"; import Image from "next/image"; import QuoteDecor from "./QuoteDecor"; +import { logger } from "../../lib/logger"; interface QuoteBlockProps { variant?: "compact" | "standard" | "extended"; @@ -109,7 +110,7 @@ const QuoteBlock = memo( // Error handling functions const handleImageError = (error: unknown) => { - console.warn( + logger.warn( `QuoteBlock: Failed to load avatar image for ${author}:`, error, ); @@ -135,7 +136,7 @@ const QuoteBlock = memo( // Validate required props if (!quote || !author) { - console.error("QuoteBlock: Missing required props (quote or author)"); + logger.error("QuoteBlock: Missing required props (quote or author)"); if (onError) { onError({ type: "missing_props", diff --git a/app/components/RuleStack.tsx b/app/components/RuleStack.tsx index e98ce2e..d56880e 100644 --- a/app/components/RuleStack.tsx +++ b/app/components/RuleStack.tsx @@ -5,6 +5,7 @@ import Image from "next/image"; import RuleCard from "./RuleCard"; import Button from "./Button"; import { getAssetPath } from "../../lib/assetUtils"; +import { logger } from "../../lib/logger"; interface RuleStackProps { className?: string; @@ -38,7 +39,7 @@ const RuleStack = memo(({ className = "" }) => { }); } } - console.log(`${templateName} template clicked`); + logger.debug(`${templateName} template clicked`); }; return ( diff --git a/app/components/WebVitalsDashboard.tsx b/app/components/WebVitalsDashboard.tsx index 733a039..fa556a8 100644 --- a/app/components/WebVitalsDashboard.tsx +++ b/app/components/WebVitalsDashboard.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, memo } from "react"; +import { logger } from "../../lib/logger"; interface VitalData { value: number; @@ -50,7 +51,7 @@ const WebVitalsDashboard = memo(() => { const data = (await response.json()) as { metrics?: Metrics }; setMetrics(data.metrics || {}); } catch (error) { - console.error("Error fetching web vitals:", error); + logger.error("Error fetching web vitals:", error); } finally { setLoading(false); } diff --git a/eslint.config.mjs b/eslint.config.mjs index 2eb8862..7ab6f77 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -129,7 +129,26 @@ const eslintConfig = [ rules: { // Basic rules "react/no-unescaped-entities": "off", - "no-console": "warn", + // Default: discourage console usage, but allow warn/error as "standard practice" + "no-console": ["warn", { allow: ["warn", "error"] }], + }, + }, + // App/lib code: no console.* (enforced) + { + files: ["app/**/*.{ts,tsx,js,jsx}", "lib/**/*.{ts,tsx,js,jsx}"], + rules: { + "no-console": "error", + }, + }, + // Tests/Storybook/scripts: console is acceptable + { + files: [ + "tests/**/*.{ts,tsx,js,jsx}", + "stories/**/*.{ts,tsx,js,jsx}", + "scripts/**/*.{ts,js}", + ], + rules: { + "no-console": "off", }, }, // Config files - allow Node.js globals diff --git a/lib/cache.ts b/lib/cache.ts index 1e7e714..9702625 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -2,6 +2,8 @@ * Content caching utilities for improved performance */ +import { logger } from "./logger"; + // In-memory cache for blog posts const blogPostCache = new Map>(); const blogListCache = new Map>(); @@ -243,9 +245,9 @@ export async function warmCache( cacheBlogPost(postWithSlug.slug, post); }); - console.log("Cache warmed up successfully"); + logger.info("Cache warmed up successfully"); } catch (error) { - console.error("Error warming up cache:", error); + logger.error("Error warming up cache:", error); } } @@ -258,7 +260,7 @@ export function isCacheHealthy(): boolean { clearExpiredCache(); return blogPostCache.size < MAX_CACHE_SIZE; } catch (error) { - console.error("Cache health check failed:", error); + logger.error("Cache health check failed:", error); return false; } } diff --git a/lib/content.ts b/lib/content.ts index cb6efda..2c2ceb1 100644 --- a/lib/content.ts +++ b/lib/content.ts @@ -3,6 +3,7 @@ import path from "path"; import matter from "gray-matter"; import { validateBlogPost, sanitizeBlogPost } from "./validation"; import type { BlogPostFrontmatter } from "./validation"; +import { logger } from "./logger"; /** * Content processing utilities for blog posts @@ -73,7 +74,7 @@ export function getBlogPostFiles(): string[] { (file) => file.endsWith(".md") || file.endsWith(".mdx"), ); } catch (error) { - console.error("Error reading blog content directory:", error); + logger.error("Error reading blog content directory:", error); return []; } } @@ -92,7 +93,7 @@ export function parseBlogPost(filePath: string): BlogPost | null { const validationResult = validateBlogPost(data); if (!validationResult.isValid) { - console.error( + logger.error( `Validation errors for ${filePath}:`, validationResult.errors, ); @@ -111,7 +112,7 @@ export function parseBlogPost(filePath: string): BlogPost | null { lastModified: fs.statSync(fullPath).mtime, }; } catch (error) { - console.error(`Error parsing blog post file ${filePath}:`, error); + logger.error(`Error parsing blog post file ${filePath}:`, error); return null; } } diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 0000000..f03b706 --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,27 @@ +/* eslint-disable no-console */ +/** + * Minimal logger wrapper. + * + * - Centralizes logging so we can swap implementations later (e.g. pino/sentry). + * - Avoids sprinkling `console.*` throughout app code. + */ + +type LoggerArgs = unknown[]; + +const isProd = process.env.NODE_ENV === "production"; + +export const logger = { + debug: (...args: LoggerArgs) => { + if (!isProd) console.debug(...args); + }, + info: (...args: LoggerArgs) => { + console.info(...args); + }, + warn: (...args: LoggerArgs) => { + console.warn(...args); + }, + error: (...args: LoggerArgs) => { + console.error(...args); + }, +}; +