From edf8a486b28582e16805945ca8e906a5e3fb0f35 Mon Sep 17 00:00:00 2001
From: ColdByDefault
Date: Mon, 16 Feb 2026 14:24:24 +0100
Subject: [PATCH 1/6] feat: enhance security measures and API functionality
across multiple routes
---
.gitignore | 1 +
SECURITY.md | 41 +++++--
app/api/about/route.ts | 40 ++++++-
app/api/admin/blog/route.ts | 51 +++++----
app/api/blog/[slug]/route.ts | 36 ++++--
app/api/blog/route.ts | 52 ++++++---
app/api/chatbot/route.ts | 139 +++--------------------
app/api/email-rewrite/analyze/route.ts | 43 ++++---
app/api/email-rewrite/remaining/route.ts | 10 +-
app/api/email-rewrite/rewriter/route.ts | 48 +++++---
next.config.ts | 4 +-
11 files changed, 248 insertions(+), 217 deletions(-)
diff --git a/.gitignore b/.gitignore
index e52d5b7..8fd475c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,4 @@ freelancer.instructions.md
/lib/generated/prisma
# Private Notes
notes.md
+desktop.ini
diff --git a/SECURITY.md b/SECURITY.md
index 3f9d4d8..9331676 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,21 +1,38 @@
# Security Policy
+## Overview
+
+This security policy outlines how we handle security vulnerabilities and the security measures implemented in this portfolio project.
+
## Supported Versions
-Use this section to tell people about which versions of your project are
-currently being supported with security updates.
+Currently supported versions with security updates:
-| Version | Supported |
-| ------- | ------------------ |
-| 5.0.x | :white_check_mark: |
-| 4.0.x | :white_check_mark: |
-| 3.0.x | :white_check_mark: |
-| < 2.0 | :x: |
+| Version | Supported | Notes |
+| ------- | ------------------ | ------------------------------- |
+| Latest | :white_check_mark: | Active development and security |
+| < 3.0 | :x: | Legacy versions not supported |
## Reporting a Vulnerability
-Use this section to tell people how to report a vulnerability.
+If you discover a security vulnerability, please follow these steps:
+
+### Where to Report
+
+**Email**: See Contact Information.
+
+## Security Audit History
+
+| Date | Type | Status | Notes |
+| ---------- | -------------- | ------ | ----------------------------- |
+| 2026-02-16 | Internal Audit | ✅ | Security improvements applied |
+
+## Additional Resources
+
+- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
+- [Next.js Security](https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy)
+- [Prisma Security](https://www.prisma.io/docs/guides/database/advanced-database-tasks/sql-injection)
+
+---
-Tell them where to go, how often they can expect to get an update on a
-reported vulnerability, what to expect if the vulnerability is accepted or
-declined, etc.
+**Copyright © 2026 ColdByDefault. All Rights Reserved.**
diff --git a/app/api/about/route.ts b/app/api/about/route.ts
index fe51d9b..847a091 100644
--- a/app/api/about/route.ts
+++ b/app/api/about/route.ts
@@ -3,11 +3,39 @@
* @copyright 2026 ColdByDefault. All Rights Reserved.
*/
+import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { aboutData } from "@/data/main/aboutData";
import aboutProfile from "@/data/main/aboutProfile.json";
+import { RateLimiter } from "@/lib/security";
+
+// Rate limiter instance: 30 requests per minute
+const rateLimiter = new RateLimiter(60000, 30);
+
+function getClientIP(request: NextRequest): string {
+ const forwarded = request.headers.get("x-forwarded-for");
+ const realIp = request.headers.get("x-real-ip");
+ const cfConnectingIp = request.headers.get("cf-connecting-ip");
+ return cfConnectingIp || realIp || forwarded?.split(",")[0] || "127.0.0.1";
+}
+
+export function GET(request: NextRequest) {
+ // Rate limiting check
+ const clientIP = getClientIP(request);
+ if (!rateLimiter.isAllowed(clientIP)) {
+ return NextResponse.json(
+ { error: "Too many requests" },
+ {
+ status: 429,
+ headers: {
+ "Retry-After": "60",
+ "X-RateLimit-Limit": "30",
+ "X-RateLimit-Remaining": "0",
+ },
+ },
+ );
+ }
-export function GET() {
try {
const combinedData = {
...aboutData,
@@ -27,13 +55,19 @@ export function GET() {
status: 200,
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=86400",
+ "X-Content-Type-Options": "nosniff",
},
});
} catch (error) {
console.error("Error fetching about data:", error);
return NextResponse.json(
{ error: "Failed to fetch about data" },
- { status: 500 }
+ {
+ status: 500,
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ },
+ },
);
}
-}
\ No newline at end of file
+}
diff --git a/app/api/admin/blog/route.ts b/app/api/admin/blog/route.ts
index e827e06..50d3a50 100644
--- a/app/api/admin/blog/route.ts
+++ b/app/api/admin/blog/route.ts
@@ -2,7 +2,7 @@
* Blog Admin API Route
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
+ */
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
@@ -59,7 +59,7 @@ function getClientIP(request: NextRequest): string {
function isAuthorized(request: NextRequest): boolean {
if (!ADMIN_TOKEN) {
- console.error("ADMIN_TOKEN environment variable not set");
+ // Security: Don't log sensitive information about environment configuration
return false;
}
@@ -188,7 +188,7 @@ const updateBlogSchema = z.object({
});
export async function GET(
- request: NextRequest
+ request: NextRequest,
): Promise> {
if (!isAuthorized(request)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -218,7 +218,7 @@ export async function GET(
const page = parseInt(searchParams.get("page") || "1", 10);
const limit = Math.min(
parseInt(searchParams.get("limit") || "20", 10),
- 50
+ 50,
);
const search = searchParams.get("search") || undefined;
const language = searchParams.get("language") || undefined;
@@ -226,14 +226,14 @@ export async function GET(
searchParams.get("published") === "true"
? true
: searchParams.get("published") === "false"
- ? false
- : undefined;
+ ? false
+ : undefined;
const featured =
searchParams.get("featured") === "true"
? true
: searchParams.get("featured") === "false"
- ? false
- : undefined;
+ ? false
+ : undefined;
const queryParams: Partial = {
page,
@@ -248,7 +248,7 @@ export async function GET(
const result = await getAdminBlogs(
context,
- queryParams as BlogListQuery
+ queryParams as BlogListQuery,
);
return NextResponse.json({ success: true, data: result });
@@ -258,7 +258,7 @@ export async function GET(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -266,7 +266,7 @@ export async function GET(
if (!blog) {
return NextResponse.json(
{ error: "Blog not found" },
- { status: 404 }
+ { status: 404 },
);
}
@@ -304,7 +304,7 @@ export async function GET(
}
export async function POST(
- request: NextRequest
+ request: NextRequest,
): Promise> {
if (!isAuthorized(request)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -324,7 +324,7 @@ export async function POST(
if (!contentType?.includes("application/json")) {
return NextResponse.json(
{ error: "Content-Type must be application/json" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -334,7 +334,7 @@ export async function POST(
} catch {
return NextResponse.json(
{ error: "Invalid JSON in request body" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -349,13 +349,13 @@ export async function POST(
error: "Validation failed",
details: parseResult.error.issues.map((issue) => issue.message),
},
- { status: 400 }
+ { status: 400 },
);
}
const blog = await createBlog(
context,
- parseResult.data as CreateBlogRequest
+ parseResult.data as CreateBlogRequest,
);
return NextResponse.json({
@@ -369,7 +369,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required for update" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -379,17 +379,18 @@ export async function POST(
{
error: "Validation failed",
details: parseResult.error.issues.map(
- (issue: ZodIssue) => `${issue.path.join(".")}: ${issue.message}`
+ (issue: ZodIssue) =>
+ `${issue.path.join(".")}: ${issue.message}`,
),
},
- { status: 400 }
+ { status: 400 },
);
}
const blog = await updateBlog(
context,
blogId,
- parseResult.data as UpdateBlogRequest
+ parseResult.data as UpdateBlogRequest,
);
return NextResponse.json({
@@ -403,7 +404,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required for deletion" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -419,7 +420,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -438,7 +439,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -456,7 +457,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -473,7 +474,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required" },
- { status: 400 }
+ { status: 400 },
);
}
diff --git a/app/api/blog/[slug]/route.ts b/app/api/blog/[slug]/route.ts
index 42303ca..4d094d8 100644
--- a/app/api/blog/[slug]/route.ts
+++ b/app/api/blog/[slug]/route.ts
@@ -1,7 +1,7 @@
/**
* @author ColdByDefault
* @copyright 2025 ColdByDefault. All Rights Reserved.
-*/
+ */
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
@@ -25,7 +25,7 @@ const blogSlugSchema = z.object({
export async function GET(
request: NextRequest,
- { params }: { params: Promise<{ slug: string }> }
+ { params }: { params: Promise<{ slug: string }> },
) {
try {
// Rate limiting check
@@ -40,8 +40,9 @@ export async function GET(
status: 429,
headers: {
"Retry-After": "60",
+ "X-Content-Type-Options": "nosniff",
},
- }
+ },
);
}
@@ -55,7 +56,12 @@ export async function GET(
error: "Invalid blog slug",
details: parseResult.error.issues.map((issue) => issue.message),
},
- { status: 400 }
+ {
+ status: 400,
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ },
+ },
);
}
@@ -63,7 +69,15 @@ export async function GET(
const blog = await getBlogBySlug(slug);
if (!blog) {
- return NextResponse.json({ error: "Blog not found" }, { status: 404 });
+ return NextResponse.json(
+ { error: "Blog not found" },
+ {
+ status: 404,
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ },
+ },
+ );
}
// Generate SEO metadata for the blog
@@ -87,14 +101,20 @@ export async function GET(
{
headers: {
"Cache-Control": "public, max-age=600, stale-while-revalidate=1200", // 10 min cache, 20 min stale
+ "X-Content-Type-Options": "nosniff",
},
- }
+ },
);
} catch (error) {
console.error("Error fetching blog:", error);
return NextResponse.json(
- { error: "Internal server error" },
- { status: 500 }
+ { error: "Failed to fetch blog" },
+ {
+ status: 500,
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ },
+ },
);
}
}
diff --git a/app/api/blog/route.ts b/app/api/blog/route.ts
index f879d7c..739dcb5 100644
--- a/app/api/blog/route.ts
+++ b/app/api/blog/route.ts
@@ -1,14 +1,41 @@
/**
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
+ */
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getBlogs } from "@/lib/hubs/blogs";
import type { BlogListQuery, BlogLanguage } from "@/types/hubs/blogs";
+import { RateLimiter } from "@/lib/security";
+
+// Rate limiter instance: 30 requests per minute
+const rateLimiter = new RateLimiter(60000, 30);
+
+function getClientIP(request: NextRequest): string {
+ const forwarded = request.headers.get("x-forwarded-for");
+ const realIp = request.headers.get("x-real-ip");
+ const cfConnectingIp = request.headers.get("cf-connecting-ip");
+ return cfConnectingIp || realIp || forwarded?.split(",")[0] || "127.0.0.1";
+}
export async function GET(request: NextRequest) {
+ // Rate limiting check
+ const clientIP = getClientIP(request);
+ if (!rateLimiter.isAllowed(clientIP)) {
+ return NextResponse.json(
+ { error: "Too many requests" },
+ {
+ status: 429,
+ headers: {
+ "Retry-After": "60",
+ "X-RateLimit-Limit": "30",
+ "X-RateLimit-Remaining": "0",
+ },
+ },
+ );
+ }
+
try {
const { searchParams } = new URL(request.url);
@@ -32,26 +59,23 @@ export async function GET(request: NextRequest) {
return NextResponse.json(result, {
headers: {
- "Cache-Control": "no-cache",
- "Access-Control-Allow-Origin": "*",
- "Access-Control-Allow-Methods": "GET",
- "Access-Control-Allow-Headers": "Content-Type",
+ "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
+ "X-Content-Type-Options": "nosniff",
+ "X-Robots-Tag": "noindex, nofollow",
},
});
} catch (error) {
console.error("Error fetching blogs:", error);
return NextResponse.json(
{
- error: "Internal server error",
- message: error instanceof Error ? error.message : "Unknown error",
- stack:
- process.env.NODE_ENV === "development"
- ? error instanceof Error
- ? error.stack
- : undefined
- : undefined,
+ error: "Failed to fetch blogs",
+ },
+ {
+ status: 500,
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ },
},
- { status: 500 }
);
}
}
diff --git a/app/api/chatbot/route.ts b/app/api/chatbot/route.ts
index c376727..4c7ec52 100644
--- a/app/api/chatbot/route.ts
+++ b/app/api/chatbot/route.ts
@@ -1,5 +1,5 @@
/**
- * ChatBot API Route with Gemini AI Integration
+ * ChatBot API Route with Groq AI Integration
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
*/
@@ -23,7 +23,6 @@ import {
} from "@/data/main/chatbot-system-prompt";
// Environment configuration with validation
-const GEMINI_API_KEY = process.env.GEMINI_KEY;
const GROQ_API_KEY = process.env.GROQ_API_KEY;
const GROQ_MODEL = process.env.GROQ_MODEL || "openai/gpt-oss-120b";
const CHATBOT_ENABLED = process.env.CHATBOT_ENABLED === "true";
@@ -177,13 +176,13 @@ function cleanupRateLimits(): void {
}
}
-// Groq API fallback when Gemini quota is exceeded
+// Groq API primary implementation
async function callGroqAPI(
messages: ChatMessage[],
systemPrompt: string
): Promise {
if (!GROQ_API_KEY) {
- throw new Error("No fallback API available");
+ throw new Error("Groq API key not configured");
}
const groqMessages = [
@@ -235,126 +234,7 @@ async function callGroqAPI(
return data.choices[0].message.content;
}
-async function callGeminiAPI(messages: ChatMessage[]): Promise {
- if (!GEMINI_API_KEY) {
- throw new Error("Gemini API key not configured");
- }
-
- // Check if this is the first user message (only 1 message = the user's first message)
- const isFirstMessage = messages.length === 1;
-
- // Convert messages to Gemini format
- const contents = messages
- .filter((msg) => msg.role !== "system")
- .map((msg) => ({
- role: msg.role === "assistant" ? "model" : "user",
- parts: [{ text: msg.content }],
- }));
-
- // Modify system prompt for first message to include greeting instruction
- let systemPrompt = chatbotConfig.systemPrompt;
- if (isFirstMessage) {
- systemPrompt += `\n\nIMPORTANT: This is the user's FIRST message in this conversation. You MUST start your response with a casual greeting like "What's up!" or "Hola!" or "How you doing!" followed by a brief introduction about yourself and what you can help with.`;
- }
-
- // Add system prompt as the first message
- const systemMessage = {
- role: "user" as const,
- parts: [{ text: systemPrompt }],
- };
-
- const requestBody = {
- contents: [systemMessage, ...contents],
- generationConfig: {
- temperature: 0.7,
- topK: 40,
- topP: 0.95,
- maxOutputTokens: 1024,
- },
- safetySettings: [
- {
- category: "HARM_CATEGORY_HARASSMENT",
- threshold: "BLOCK_MEDIUM_AND_ABOVE",
- },
- {
- category: "HARM_CATEGORY_HATE_SPEECH",
- threshold: "BLOCK_MEDIUM_AND_ABOVE",
- },
- {
- category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
- threshold: "BLOCK_MEDIUM_AND_ABOVE",
- },
- {
- category: "HARM_CATEGORY_DANGEROUS_CONTENT",
- threshold: "BLOCK_MEDIUM_AND_ABOVE",
- },
- ],
- };
-
- try {
- const response = await fetch(
- `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(requestBody),
- }
- );
-
- if (!response.ok) {
- const errorData = (await response.json().catch(() => ({}))) as {
- error?: { message?: string; code?: number };
- };
-
- // Handle quota exceeded (429) specifically
- if (response.status === 429) {
- const retryMatch =
- errorData.error?.message?.match(/retry in ([\d.]+)s/i);
- const retryAfter = retryMatch?.[1] ? parseFloat(retryMatch[1]) : 60;
- throw new Error(
- `QUOTA_EXCEEDED:${retryAfter}:Gemini API quota exceeded. Please try again later.`
- );
- }
-
- throw new Error(
- `Gemini API error: ${response.status} - ${
- errorData.error?.message || "Unknown error"
- }`
- );
- }
-
- const data = (await response.json()) as {
- candidates?: Array<{
- content?: {
- parts?: Array<{ text?: string }>;
- };
- }>;
- };
-
- if (!data.candidates?.[0]?.content?.parts?.[0]?.text) {
- throw new Error("Invalid response format from Gemini API");
- }
- return data.candidates[0].content.parts[0].text;
- } catch (error) {
- console.error("Gemini API call failed:", error);
-
- // Try Groq as fallback if Gemini quota exceeded and Groq key available
- if (
- GROQ_API_KEY &&
- error instanceof Error &&
- (error.message.includes("QUOTA_EXCEEDED") ||
- error.message.includes("429"))
- ) {
- console.log("Falling back to Groq API...");
- return callGroqAPI(messages, systemPrompt);
- }
-
- throw error;
- }
-}
// API Routes
export async function POST(
@@ -449,8 +329,17 @@ export async function POST(
// Add user message to session
sessionMessages.push(userMessage);
- // Call Gemini AI
- const aiResponse = await callGeminiAPI(sessionMessages);
+ // Check if this is the first user message (only 1 message = the user's first message)
+ const isFirstMessage = sessionMessages.length === 1;
+
+ // Modify system prompt for first message to include greeting instruction
+ let systemPrompt = chatbotConfig.systemPrompt;
+ if (isFirstMessage) {
+ systemPrompt += `\n\nIMPORTANT: This is the user's FIRST message in this conversation. You MUST start your response with a casual greeting like "What's up!" or "Hola!" or "How you doing!" followed by a brief introduction about yourself and what you can help with.`;
+ }
+
+ // Call Groq AI
+ const aiResponse = await callGroqAPI(sessionMessages, systemPrompt);
// Create assistant message
const assistantMessage: ChatMessage = {
diff --git a/app/api/email-rewrite/analyze/route.ts b/app/api/email-rewrite/analyze/route.ts
index 61b7989..10610b0 100644
--- a/app/api/email-rewrite/analyze/route.ts
+++ b/app/api/email-rewrite/analyze/route.ts
@@ -34,7 +34,7 @@ const analyzeRequestSchema = z.object({
.string()
.max(
MAX_CONTEXT_LENGTH,
- `Context must be under ${MAX_CONTEXT_LENGTH} characters`
+ `Context must be under ${MAX_CONTEXT_LENGTH} characters`,
)
.optional()
.transform((val) => (val ? sanitizeChatInput(val) : undefined)),
@@ -50,7 +50,7 @@ function getClientIP(request: NextRequest): string {
async function callGroqAPI(
email: string,
systemPrompt: string,
- context?: string
+ context?: string,
): Promise {
if (!GROQ_API_KEY) {
throw new Error("Groq API key not configured");
@@ -85,7 +85,7 @@ async function callGroqAPI(
top_p: 1,
stream: false,
}),
- }
+ },
);
if (!response.ok) {
@@ -95,7 +95,7 @@ async function callGroqAPI(
throw new Error(
`Groq API error: ${response.status} - ${
errorData.error?.message || "Unknown error"
- }`
+ }`,
);
}
@@ -115,7 +115,7 @@ export async function POST(request: NextRequest) {
if (!REWRITER_ENABLED) {
return NextResponse.json(
{ error: "Email analyzer service is currently disabled" },
- { status: 503 }
+ { status: 503 },
);
}
@@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
console.error("GROQ_API_KEY not configured");
return NextResponse.json(
{ error: "Service configuration error" },
- { status: 500 }
+ { status: 500 },
);
}
@@ -136,7 +136,7 @@ export async function POST(request: NextRequest) {
error: "Rate limit exceeded. Please try again later.",
remaining: 0,
},
- { status: 429 }
+ { status: 429 },
);
}
@@ -145,7 +145,7 @@ export async function POST(request: NextRequest) {
if (!body) {
return NextResponse.json(
{ error: "Invalid request body" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -155,7 +155,7 @@ export async function POST(request: NextRequest) {
const firstError = validationResult.error.issues[0];
return NextResponse.json(
{ error: firstError?.message || "Invalid request data" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -176,7 +176,7 @@ export async function POST(request: NextRequest) {
console.error("Failed to parse AI response:", rawResponse);
return NextResponse.json(
{ error: "Failed to parse AI response" },
- { status: 500 }
+ { status: 500 },
);
}
@@ -192,8 +192,10 @@ export async function POST(request: NextRequest) {
status: 200,
headers: {
"X-RateLimit-Remaining": remaining.toString(),
+ "X-Content-Type-Options": "nosniff",
+ "Cache-Control": "no-store, no-cache, must-revalidate",
},
- }
+ },
);
} catch (error) {
console.error("Email analyze error:", error);
@@ -204,10 +206,23 @@ export async function POST(request: NextRequest) {
{
error: "AI service temporarily unavailable. Please try again later.",
},
- { status: 503 }
+ {
+ status: 503,
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ },
+ },
);
}
- return NextResponse.json({ error: sanitizedError }, { status: 500 });
+ return NextResponse.json(
+ { error: sanitizedError },
+ {
+ status: 500,
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ },
+ },
+ );
}
-}
\ No newline at end of file
+}
diff --git a/app/api/email-rewrite/remaining/route.ts b/app/api/email-rewrite/remaining/route.ts
index 6e011a5..bf3c66d 100644
--- a/app/api/email-rewrite/remaining/route.ts
+++ b/app/api/email-rewrite/remaining/route.ts
@@ -13,5 +13,13 @@ export async function GET() {
const remaining = getRemainingUses(ip);
- return Response.json({ remaining });
+ return Response.json(
+ { remaining },
+ {
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ "Cache-Control": "no-store, no-cache, must-revalidate",
+ },
+ },
+ );
}
diff --git a/app/api/email-rewrite/rewriter/route.ts b/app/api/email-rewrite/rewriter/route.ts
index d5f0fb8..af25755 100644
--- a/app/api/email-rewrite/rewriter/route.ts
+++ b/app/api/email-rewrite/rewriter/route.ts
@@ -2,7 +2,7 @@
* Email Rewriter API Route with Groq AI Integration
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
+ */
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
@@ -53,7 +53,7 @@ function getClientIP(request: NextRequest): string {
*/
async function callGroqAPI(
email: string,
- systemPrompt: string
+ systemPrompt: string,
): Promise {
if (!GROQ_API_KEY) {
throw new Error("Groq API key not configured");
@@ -83,7 +83,7 @@ async function callGroqAPI(
top_p: 1,
stream: false,
}),
- }
+ },
);
if (!response.ok) {
@@ -93,7 +93,7 @@ async function callGroqAPI(
throw new Error(
`Groq API error: ${response.status} - ${
errorData.error?.message || "Unknown error"
- }`
+ }`,
);
}
@@ -114,7 +114,7 @@ export async function POST(request: NextRequest) {
if (!REWRITER_ENABLED) {
return NextResponse.json(
{ error: "Email rewriter service is currently disabled" },
- { status: 503 }
+ { status: 503 },
);
}
@@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
console.error("GROQ_API_KEY not configured");
return NextResponse.json(
{ error: "Service configuration error" },
- { status: 500 }
+ { status: 500 },
);
}
@@ -145,7 +145,7 @@ export async function POST(request: NextRequest) {
"Retry-After": "86400", // 24 hours in seconds
"X-RateLimit-Remaining": "0",
},
- }
+ },
);
}
@@ -155,7 +155,7 @@ export async function POST(request: NextRequest) {
if (!body) {
return NextResponse.json(
{ error: "Invalid request body" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -166,7 +166,7 @@ export async function POST(request: NextRequest) {
const firstError = validationResult.error.issues[0];
return NextResponse.json(
{ error: firstError?.message || "Invalid request data" },
- { status: 400 }
+ { status: 400 },
);
}
@@ -178,7 +178,7 @@ export async function POST(request: NextRequest) {
if (!systemPrompt) {
return NextResponse.json(
{ error: "Invalid tone configuration" },
- { status: 500 }
+ { status: 500 },
);
}
@@ -198,8 +198,10 @@ export async function POST(request: NextRequest) {
status: 200,
headers: {
"X-RateLimit-Remaining": remaining.toString(),
+ "X-Content-Type-Options": "nosniff",
+ "Cache-Control": "no-store, no-cache, must-revalidate",
},
- }
+ },
);
} catch (error) {
console.error("Email rewrite error:", error);
@@ -215,18 +217,36 @@ export async function POST(request: NextRequest) {
error:
"AI service temporarily unavailable. Please try again later.",
},
- { status: 503 }
+ {
+ status: 503,
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ },
+ },
);
}
if (error.message.includes("rate limit")) {
return NextResponse.json(
{ error: "Service rate limit reached. Please try again later." },
- { status: 429 }
+ {
+ status: 429,
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ },
+ },
);
}
}
- return NextResponse.json({ error: sanitizedError }, { status: 500 });
+ return NextResponse.json(
+ { error: sanitizedError },
+ {
+ status: 500,
+ headers: {
+ "X-Content-Type-Options": "nosniff",
+ },
+ },
+ );
}
}
diff --git a/next.config.ts b/next.config.ts
index fd27412..eaaa9cc 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -99,11 +99,13 @@ const nextConfig: NextConfig = {
key: "Content-Security-Policy",
value: [
"default-src 'self'",
+ // TODO: Remove 'unsafe-inline' and 'unsafe-eval' by implementing CSP nonces for Next.js
+ // Current limitation: Required for Next.js runtime and Vercel Analytics
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://vercel.live https://va.vercel-scripts.com",
"style-src 'self' 'unsafe-inline'", // Allow inline styles for Tailwind and components
"img-src 'self' data: blob: https://avatars.githubusercontent.com https://github.com",
"font-src 'self' data:",
- "connect-src 'self' https://api.github.com https://www.googleapis.com https://generativelanguage.googleapis.com https://vercel.live https://vitals.vercel-analytics.com",
+ "connect-src 'self' https://api.github.com https://www.googleapis.com https://generativelanguage.googleapis.com https://vercel.live https://vitals.vercel-analytics.com https://api.groq.com",
"frame-src 'none'",
"object-src 'none'",
"base-uri 'self'",
From 9ba51d4455aa725c4f9e167fd94897affcfc3bde Mon Sep 17 00:00:00 2001
From: ColdByDefault
Date: Mon, 16 Feb 2026 14:28:21 +0100
Subject: [PATCH 2/6] feat: update chatbot integration titles and descriptions
across multiple languages
---
app/(legals)/privacy/page.tsx | 20 ++++++------------
app/api/chatbot/route.ts | 38 +++++++++++++++++------------------
messages/de.json | 4 ++--
messages/en.json | 4 ++--
messages/es.json | 4 ++--
messages/fr.json | 4 ++--
messages/sv.json | 4 ++--
7 files changed, 34 insertions(+), 44 deletions(-)
diff --git a/app/(legals)/privacy/page.tsx b/app/(legals)/privacy/page.tsx
index dba0f46..f571d38 100644
--- a/app/(legals)/privacy/page.tsx
+++ b/app/(legals)/privacy/page.tsx
@@ -1,7 +1,7 @@
/**
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
+ */
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
@@ -92,9 +92,7 @@ export default async function Privacy() {
{t("dataProcessing.vercelTitle")}
-
- {t("dataProcessing.vercelDescription")}
-
+ {t("dataProcessing.vercelDescription")}
@@ -112,19 +110,15 @@ export default async function Privacy() {
- {t("chatbot.geminiTitle")}
-
-
- {t("chatbot.geminiDescription")}
+ {t("chatbot.apiTitle")}
+
{t("chatbot.apiDescription")}
{t("chatbot.temporaryTitle")}
-
- {t("chatbot.temporaryDescription")}
-
+
{t("chatbot.temporaryDescription")}
@@ -144,9 +138,7 @@ export default async function Privacy() {
{t("booking.calendlyTitle")}
-
- {t("booking.calendlyDescription")}
-
+ {t("booking.calendlyDescription")}
diff --git a/app/api/chatbot/route.ts b/app/api/chatbot/route.ts
index 4c7ec52..977080c 100644
--- a/app/api/chatbot/route.ts
+++ b/app/api/chatbot/route.ts
@@ -30,23 +30,23 @@ const CHATBOT_ENABLED = process.env.CHATBOT_ENABLED === "true";
const chatbotConfig: ChatBotConfig = {
maxMessagesPerSession: parseInt(
process.env.CHATBOT_MAX_MESSAGES_PER_SESSION || "20",
- 10
+ 10,
),
maxMessageLength: parseInt(
process.env.CHATBOT_MAX_MESSAGE_LENGTH || "1000",
- 10
+ 10,
),
rateLimitPerMinute: parseInt(
process.env.CHATBOT_RATE_LIMIT_PER_MINUTE || "10",
- 10
+ 10,
),
rateLimitPerHour: parseInt(
process.env.CHATBOT_RATE_LIMIT_PER_HOUR || "50",
- 10
+ 10,
),
sessionTimeoutMs: parseInt(
process.env.CHATBOT_SESSION_TIMEOUT_MS || "1800000",
- 10
+ 10,
),
systemPrompt: REEM_SYSTEM_PROMPT,
};
@@ -144,7 +144,7 @@ function getRateLimitInfo(clientIP: string): {
const minuteRemaining = Math.max(
0,
- chatbotConfig.rateLimitPerMinute - limit.minute.count
+ chatbotConfig.rateLimitPerMinute - limit.minute.count,
);
const nextMinuteReset = limit.minute.windowStart + 60000;
@@ -158,7 +158,7 @@ function cleanupSessions(): void {
const now = Date.now();
for (const [sessionId, messages] of sessions.entries()) {
const lastActivity = Math.max(
- ...messages.map((m) => m.timestamp.getTime())
+ ...messages.map((m) => m.timestamp.getTime()),
);
if (now - lastActivity > chatbotConfig.sessionTimeoutMs) {
sessions.delete(sessionId);
@@ -179,7 +179,7 @@ function cleanupRateLimits(): void {
// Groq API primary implementation
async function callGroqAPI(
messages: ChatMessage[],
- systemPrompt: string
+ systemPrompt: string,
): Promise {
if (!GROQ_API_KEY) {
throw new Error("Groq API key not configured");
@@ -209,7 +209,7 @@ async function callGroqAPI(
temperature: 0.7,
max_tokens: 1024,
}),
- }
+ },
);
if (!response.ok) {
@@ -219,7 +219,7 @@ async function callGroqAPI(
throw new Error(
`Groq API error: ${response.status} - ${
errorData.error?.message || "Unknown error"
- }`
+ }`,
);
}
@@ -234,11 +234,9 @@ async function callGroqAPI(
return data.choices[0].message.content;
}
-
-
// API Routes
export async function POST(
- request: NextRequest
+ request: NextRequest,
): Promise> {
// Check if chatbot is enabled
if (!CHATBOT_ENABLED) {
@@ -247,7 +245,7 @@ export async function POST(
error: "ChatBot service is currently unavailable",
code: "SERVICE_UNAVAILABLE",
},
- { status: 503 }
+ { status: 503 },
);
}
@@ -262,7 +260,7 @@ export async function POST(
code: "RATE_LIMIT_EXCEEDED",
details: rateLimitInfo,
},
- { status: 429 }
+ { status: 429 },
);
}
@@ -275,7 +273,7 @@ export async function POST(
error: "Content-Type must be application/json",
code: "INVALID_INPUT",
},
- { status: 400 }
+ { status: 400 },
);
}
@@ -296,7 +294,7 @@ export async function POST(
: "Invalid request body",
code: "INVALID_INPUT",
},
- { status: 400 }
+ { status: 400 },
);
}
@@ -313,7 +311,7 @@ export async function POST(
error: `Maximum ${chatbotConfig.maxMessagesPerSession} messages per session exceeded`,
code: "INVALID_INPUT",
},
- { status: 400 }
+ { status: 400 },
);
}
@@ -392,7 +390,7 @@ export async function POST(
headers: {
"Retry-After": String(Math.ceil(retrySeconds || 60)),
},
- }
+ },
);
}
@@ -404,7 +402,7 @@ export async function POST(
: "Internal server error",
code: "SERVICE_UNAVAILABLE",
},
- { status: 500 }
+ { status: 500 },
);
}
}
diff --git a/messages/de.json b/messages/de.json
index 965278a..cb05524 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -222,8 +222,8 @@
"chatbot": {
"title": "KI-Chatbot-Service",
"description": "Diese Website enthält einen KI-Chatbot für interaktive Unterstützung und Informationen.",
- "geminiTitle": "Groq API Integration",
- "geminiDescription": "Der Chatbot wird von Groq betrieben. Durch die Nutzung des Chatbots akzeptieren Sie die Nutzungsbedingungen und Datenschutzrichtlinien von Groq. Chat-Anfragen werden über die Groq API verarbeitet.",
+ "apiTitle": "Groq API Integration",
+ "apiDescription": "Der Chatbot wird von Groq betrieben. Durch die Nutzung des Chatbots akzeptieren Sie die Nutzungsbedingungen und Datenschutzrichtlinien von Groq. Chat-Anfragen werden über die Groq API verarbeitet.",
"temporaryTitle": "Keine Datenspeicherung",
"temporaryDescription": "Ich speichere keine Chat-Gespräche, Verläufe oder persönlichen Informationen. Alle Chat-Daten werden ausschließlich in Ihrem Browser (Web-Storage) gespeichert und verbleiben vollständig unter Ihrer Kontrolle."
},
diff --git a/messages/en.json b/messages/en.json
index fc2459e..8dfda78 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -222,8 +222,8 @@
"chatbot": {
"title": "AI Chatbot Service",
"description": "This website includes an AI chatbot for interactive assistance and information.",
- "geminiTitle": "Groq API Integration",
- "geminiDescription": "The chatbot is powered by Groq. By using the chatbot, you accept Groq's terms of service and privacy policy. Chat requests are processed via the Groq API.",
+ "apiTitle": "Groq API Integration",
+ "apiDescription": "The chatbot is powered by Groq. By using the chatbot, you accept Groq's terms of service and privacy policy. Chat requests are processed via the Groq API.",
"temporaryTitle": "No Data Storage",
"temporaryDescription": "I do not save any chat conversations, history, or personal information. All chat data is stored exclusively in your browser (web storage) and remains entirely under your control."
},
diff --git a/messages/es.json b/messages/es.json
index 441c19b..bf5a8f7 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -222,8 +222,8 @@
"chatbot": {
"title": "Servicio de Chatbot IA",
"description": "Este sitio web incluye un chatbot de IA para asistencia interactiva e información.",
- "geminiTitle": "Integración de Groq API",
- "geminiDescription": "El chatbot funciona con Groq. Al usar el chatbot, aceptas los términos de servicio y la política de privacidad de Groq. Las solicitudes del chat se procesan a través de la API de Groq.",
+ "apiTitle": "Integración de Groq API",
+ "apiDescription": "El chatbot funciona con Groq. Al usar el chatbot, aceptas los términos de servicio y la política de privacidad de Groq. Las solicitudes del chat se procesan a través de la API de Groq.",
"temporaryTitle": "Sin Almacenamiento de Datos",
"temporaryDescription": "No guardo ninguna conversación, historial o información personal. Todos los datos del chat se almacenan exclusivamente en tu navegador (almacenamiento web) y permanecen completamente bajo tu control."
},
diff --git a/messages/fr.json b/messages/fr.json
index c62c0a0..063fdc3 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -222,8 +222,8 @@
"chatbot": {
"title": "Service de Chatbot IA",
"description": "Ce site web inclut un chatbot IA pour l'assistance interactive et l'information.",
- "geminiTitle": "Intégration de l'API Groq",
- "geminiDescription": "Le chatbot est alimenté par Groq. En utilisant le chatbot, vous acceptez les conditions d'utilisation et la politique de confidentialité de Groq. Les requêtes de chat sont traitées via l'API Groq.",
+ "apiTitle": "Intégration de l'API Groq",
+ "apiDescription": "Le chatbot est alimenté par Groq. En utilisant le chatbot, vous acceptez les conditions d'utilisation et la politique de confidentialité de Groq. Les requêtes de chat sont traitées via l'API Groq.",
"temporaryTitle": "Aucun Stockage de Données",
"temporaryDescription": "Je ne sauvegarde aucune conversation, historique ou information personnelle. Toutes les données de chat sont stockées exclusivement dans votre navigateur (stockage web) et restent entièrement sous votre contrôle."
},
diff --git a/messages/sv.json b/messages/sv.json
index 53ac5e1..991e305 100644
--- a/messages/sv.json
+++ b/messages/sv.json
@@ -222,8 +222,8 @@
"chatbot": {
"title": "AI Chatbot-tjänst",
"description": "Denna webbplats inkluderar en AI-chatbot för interaktiv assistans och information.",
- "geminiTitle": "Groq API Integration",
- "geminiDescription": "Chatboten drivs av Groq. Genom att använda chatboten accepterar du Groqs användarvillkor och integritetspolicy. Chattförfrågningar bearbetas via Groq API.",
+ "apiTitle": "Groq API Integration",
+ "apiDescription": "Chatboten drivs av Groq. Genom att använda chatboten accepterar du Groqs användarvillkor och integritetspolicy. Chattförfrågningar bearbetas via Groq API.",
"temporaryTitle": "Ingen Datalagring",
"temporaryDescription": "Jag sparar inga chattkonversationer, historik eller personlig information. All chattdata lagras exklusivt i din webbläsare (webblagring) och förblir helt under din kontroll."
},
From 61d3cb07314f8e3c76dbc19fd808cfa8e1d2ad24 Mon Sep 17 00:00:00 2001
From: ColdByDefault
Date: Mon, 16 Feb 2026 14:33:06 +0100
Subject: [PATCH 3/6] refactor: remove PageSpeed API routes and related
components
- Deleted the pagespeed refresh route and its associated logic.
- Removed the main pagespeed route and its caching mechanism.
- Eliminated the PageSpeedInsights component and its related hooks.
- Updated the home page to remove references to PageSpeed insights.
- Cleaned up unused types and data related to PageSpeed metrics.
---
README.md | 2 +-
app/api/pagespeed/refresh/route.ts | 151 -------
app/api/pagespeed/route.ts | 469 ---------------------
app/page.tsx | 40 +-
components/pagespeed/PageSpeedInsights.tsx | 348 ---------------
components/pagespeed/index.ts | 7 -
data/hubs/portfolio-section.data.ts | 47 +--
hooks/use-pageSpeed-data.ts | 247 -----------
types/configs/pagespeed.ts | 64 ---
9 files changed, 8 insertions(+), 1367 deletions(-)
delete mode 100644 app/api/pagespeed/refresh/route.ts
delete mode 100644 app/api/pagespeed/route.ts
delete mode 100644 components/pagespeed/PageSpeedInsights.tsx
delete mode 100644 components/pagespeed/index.ts
delete mode 100644 hooks/use-pageSpeed-data.ts
delete mode 100644 types/configs/pagespeed.ts
diff --git a/README.md b/README.md
index 7336d4c..3feeb0e 100644
--- a/README.md
+++ b/README.md
@@ -198,7 +198,7 @@ Comprehensive API endpoints with security-first design:
| `/api/blog` | Blog content management and retrieval | Prisma + Zod |
| `/api/github` | Fetches GitHub profile + repos (filtered) | Tokenized (env) |
| `/api/pagespeed` | Surfaces PageSpeed metrics | Enhanced caching + error handling |
-| `/api/chatbot` | Interactive AI chatbot (Reem) for visitor queries | Gemini + Groq fallback |
+| `/api/chatbot` | Interactive AI chatbot (Reem) for visitor queries | Groq API |
| `/api/admin` | Administrative operations for content | Secured endpoints |
Controls:
diff --git a/app/api/pagespeed/refresh/route.ts b/app/api/pagespeed/refresh/route.ts
deleted file mode 100644
index 5da05d0..0000000
--- a/app/api/pagespeed/refresh/route.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * @author ColdByDefault
- * @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
-
-import { NextRequest, NextResponse } from "next/server";
-import type {
- PageSpeedResult,
- PageSpeedMetrics,
-} from "@/types/configs/pagespeed";
-
-interface RefreshResult {
- strategy: "mobile" | "desktop";
- success: boolean;
- data?: PageSpeedResult;
- error?: string;
-}
-
-interface RefreshErrorResult {
- error: string;
- success: false;
-}
-
-const CRON_SECRET = process.env.CRON_SECRET;
-const MAIN_URL = process.env.PORTFOLIO_URL || "https://www.coldbydefault.com";
-
-export async function POST(request: NextRequest) {
- try {
- // Verify cron secret
- const authHeader = request.headers.get("authorization");
- if (authHeader !== `Bearer ${CRON_SECRET}`) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
-
- console.log("Starting automated PageSpeed refresh...");
-
- // Use hardcoded base URL to prevent SSRF
- const baseUrl = process.env.VERCEL_URL
- ? `https://${process.env.VERCEL_URL}`
- : "https://www.coldbydefault.com";
- const strategies: ("mobile" | "desktop")[] = ["mobile", "desktop"];
-
- const refreshPromises: Promise[] = strategies.map(
- async (strategy) => {
- try {
- const response = await fetch(
- `${baseUrl}/api/pagespeed?url=${encodeURIComponent(
- MAIN_URL
- )}&strategy=${strategy}&refresh=true`,
- {
- method: "GET",
- headers: {
- "User-Agent": "Vercel-Cron/1.0",
- },
- signal: AbortSignal.timeout(60000), // 60 seconds timeout
- }
- );
-
- if (response.ok) {
- const data = (await response.json()) as PageSpeedResult;
-
- // Validate that we received the expected structure
- if (!data?.metrics || typeof data.metrics !== "object") {
- throw new Error("Invalid response structure from PageSpeed API");
- }
-
- const metrics: PageSpeedMetrics = data.metrics;
- console.log("✅ Refreshed %s data for %s:", strategy, MAIN_URL, {
- performance: metrics.performance,
- accessibility: metrics.accessibility,
- bestPractices: metrics.bestPractices,
- seo: metrics.seo,
- });
- return { strategy, success: true, data } satisfies RefreshResult;
- } else {
- console.error(
- "❌ Failed to refresh %s data:",
- strategy,
- response.status,
- response.statusText
- );
- return {
- strategy,
- success: false,
- error: `HTTP ${response.status}`,
- } satisfies RefreshResult;
- }
- } catch (error) {
- console.error("❌ Error refreshing %s data:", strategy, error);
- return {
- strategy,
- success: false,
- error: error instanceof Error ? error.message : "Unknown error",
- } satisfies RefreshResult;
- }
- }
- );
-
- const results = await Promise.allSettled(refreshPromises);
-
- const successCount = results.filter(
- (result) => result.status === "fulfilled" && result.value.success
- ).length;
-
- console.log(
- `Automated refresh completed: ${successCount}/${strategies.length} successful`
- );
-
- return NextResponse.json({
- success: true,
- message: `Refreshed ${successCount}/${strategies.length} PageSpeed datasets`,
- timestamp: new Date().toISOString(),
- results: results.map((result): RefreshResult | RefreshErrorResult =>
- result.status === "fulfilled"
- ? result.value
- : { error: "Promise rejected", success: false }
- ),
- });
- } catch (error) {
- console.error("Cron job error:", error);
- return NextResponse.json(
- {
- success: false,
- error: "Cron job failed",
- timestamp: new Date().toISOString(),
- },
- { status: 500 }
- );
- }
-}
-
-// GET method for manual testing
-export async function GET(request: NextRequest) {
- const { searchParams } = new URL(request.url);
- const secret = searchParams.get("secret");
-
- if (secret !== CRON_SECRET) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
-
- // Create a new request with authorization header for POST method
- const newRequest = new NextRequest(request.url, {
- method: "POST",
- headers: {
- ...request.headers,
- authorization: `Bearer ${CRON_SECRET}`,
- },
- });
-
- return POST(newRequest);
-}
diff --git a/app/api/pagespeed/route.ts b/app/api/pagespeed/route.ts
deleted file mode 100644
index fd0161a..0000000
--- a/app/api/pagespeed/route.ts
+++ /dev/null
@@ -1,469 +0,0 @@
-/**
- * @author ColdByDefault
- * @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
-
-import type { NextRequest } from "next/server";
-import { NextResponse } from "next/server";
-import { z } from "zod";
-import type {
- PageSpeedMetrics,
- PageSpeedResult,
- PageSpeedApiRawResponse,
-} from "@/types/configs/pagespeed";
-
-// Zod schema for SSRF protection
-const pageSpeedRequestSchema = z.object({
- url: z
- .string()
- .url("Invalid URL format")
- .refine((url) => {
- try {
- const parsed = new URL(url);
-
- // Only allow HTTP/HTTPS protocols
- if (!["http:", "https:"].includes(parsed.protocol)) {
- return false;
- }
-
- // Block localhost and private IP ranges to prevent SSRF
- const hostname = parsed.hostname.toLowerCase();
-
- // Block localhost variants
- if (["localhost", "127.0.0.1", "::1"].includes(hostname)) {
- return false;
- }
-
- // Block private IP ranges (simplified check)
- if (
- hostname.match(/^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\./)
- ) {
- return false;
- }
-
- // Block link-local addresses
- if (hostname.match(/^169\.254\.|^fe80:/)) {
- return false;
- }
-
- // Block internal domains
- if (hostname.includes(".local") || hostname.includes(".internal")) {
- return false;
- }
-
- return true;
- } catch {
- return false;
- }
- }, "URL not allowed for security reasons"),
- strategy: z.enum(["mobile", "desktop"]).default("mobile"),
- refresh: z.boolean().default(false),
-});
-
-// In-memory cache with automatic expiration
-interface CacheEntry {
- data: PageSpeedResult;
- timestamp: number;
- isRefreshing?: boolean;
- timeoutCount?: number; // Track consecutive timeout failures
-}
-
-class PageSpeedCache {
- private static instance: PageSpeedCache;
- private cache = new Map();
- private readonly CACHE_DURATION = 12 * 60 * 60 * 1000; // 12 hours
- private readonly STALE_WHILE_REVALIDATE = 24 * 60 * 60 * 1000; // 24 hours
- private readonly MAX_TIMEOUT_COUNT = 3; // Max consecutive timeouts before circuit breaker
-
- static getInstance(): PageSpeedCache {
- if (!PageSpeedCache.instance) {
- PageSpeedCache.instance = new PageSpeedCache();
- }
- return PageSpeedCache.instance;
- }
-
- getCacheKey(url: string, strategy: string): string {
- return `${url}_${strategy}`;
- }
-
- get(url: string, strategy: string): CacheEntry | null {
- const key = this.getCacheKey(url, strategy);
- const entry = this.cache.get(key);
-
- if (!entry) return null;
-
- const now = Date.now();
- const age = now - entry.timestamp;
-
- // If data is stale beyond revalidate time, remove it
- if (age > this.STALE_WHILE_REVALIDATE) {
- this.cache.delete(key);
- return null;
- }
-
- return entry;
- }
-
- set(url: string, strategy: string, data: PageSpeedResult): void {
- const key = this.getCacheKey(url, strategy);
- this.cache.set(key, {
- data,
- timestamp: Date.now(),
- isRefreshing: false,
- timeoutCount: 0, // Reset timeout count on successful fetch
- });
- }
-
- incrementTimeoutCount(url: string, strategy: string): void {
- const key = this.getCacheKey(url, strategy);
- const entry = this.cache.get(key);
- if (entry) {
- entry.timeoutCount = (entry.timeoutCount || 0) + 1;
- }
- }
-
- shouldSkipFetch(url: string, strategy: string): boolean {
- const entry = this.get(url, strategy);
- return !!entry && (entry.timeoutCount || 0) >= this.MAX_TIMEOUT_COUNT;
- }
-
- isStale(url: string, strategy: string): boolean {
- const entry = this.get(url, strategy);
- if (!entry) return true;
-
- const age = Date.now() - entry.timestamp;
- return age > this.CACHE_DURATION;
- }
-
- setRefreshing(url: string, strategy: string, refreshing: boolean): void {
- const key = this.getCacheKey(url, strategy);
- const entry = this.cache.get(key);
- if (entry) {
- entry.isRefreshing = refreshing;
- }
- }
-
- isRefreshing(url: string, strategy: string): boolean {
- const entry = this.get(url, strategy);
- return entry?.isRefreshing || false;
- }
-
- // Clean up old entries periodically
- cleanup(): void {
- const now = Date.now();
- for (const [key, entry] of this.cache.entries()) {
- if (now - entry.timestamp > this.STALE_WHILE_REVALIDATE) {
- this.cache.delete(key);
- }
- }
- }
-}
-
-const cache = PageSpeedCache.getInstance();
-
-// Background refresh function
-async function backgroundRefresh(
- url: string,
- strategy: "mobile" | "desktop"
-): Promise {
- if (cache.isRefreshing(url, strategy)) {
- return; // Already refreshing
- }
-
- cache.setRefreshing(url, strategy, true);
-
- try {
- const result = await fetchPageSpeedData(url, strategy);
- if (result) {
- cache.set(url, strategy, result);
- }
- } catch (error) {
- console.error(
- "Background refresh failed for %s (%s):",
- url,
- strategy,
- error
- );
- } finally {
- cache.setRefreshing(url, strategy, false);
- }
-}
-
-// Extracted PageSpeed API fetch function
-async function fetchPageSpeedData(
- url: string,
- strategy: "mobile" | "desktop"
-): Promise {
- const apiKey = process.env.PAGESPEED_INSIGHTS_API_KEY;
- if (!apiKey) {
- throw new Error("PageSpeed Insights API key not configured");
- }
-
- const pageSpeedUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(
- url
- )}&key=${apiKey}&strategy=${strategy}&category=performance&category=accessibility&category=best-practices&category=seo`;
-
- // Use conservative timeout to prevent Vercel function timeout
- // Desktop analysis typically takes longer than mobile
- const timeoutMs = process.env.NODE_ENV === "production" ? 40000 : 45000; // 40s for prod, 45s for dev
-
- const response = await fetch(pageSpeedUrl, {
- method: "GET",
- headers: {
- Accept: "application/json",
- "User-Agent": "Mozilla/5.0 (compatible; Portfolio/1.0)",
- },
- signal: AbortSignal.timeout(timeoutMs),
- });
-
- if (!response.ok) {
- if (response.status === 429) {
- throw new Error("Rate limit exceeded");
- }
- if (response.status === 403) {
- throw new Error(
- "PageSpeed API access denied - API key may be invalid, restricted, or the API not enabled in Google Cloud Console"
- );
- }
- if (response.status >= 500) {
- throw new Error("PageSpeed service unavailable");
- }
- throw new Error(`PageSpeed API error: ${response.status}`);
- }
-
- const data = (await response.json()) as PageSpeedApiRawResponse;
-
- if (!data?.lighthouseResult?.categories) {
- throw new Error("Invalid response from PageSpeed API");
- }
-
- const categories = data.lighthouseResult.categories;
- const metrics: PageSpeedMetrics = {
- performance: Math.round((categories.performance?.score ?? 0) * 100),
- accessibility: Math.round((categories.accessibility?.score ?? 0) * 100),
- bestPractices: Math.round((categories["best-practices"]?.score ?? 0) * 100),
- seo: Math.round((categories.seo?.score ?? 0) * 100),
- };
-
- if (categories.pwa?.score !== undefined && categories.pwa?.score !== null) {
- metrics.pwa = Math.round(categories.pwa.score * 100);
- }
-
- return {
- url: data.id ?? url,
- strategy,
- metrics,
- };
-}
-
-export async function GET(request: NextRequest) {
- try {
- // Skip PageSpeed API calls in development
- if (process.env.NODE_ENV !== "production") {
- return NextResponse.json(
- {
- url: "https://www.coldbydefault.com",
- strategy: "mobile",
- metrics: {
- performance: 0,
- accessibility: 0,
- bestPractices: 0,
- seo: 0,
- },
- disabled: true,
- message: "PageSpeed API is disabled in development mode",
- },
- { status: 200 }
- );
- }
-
- const { searchParams } = new URL(request.url);
-
- // Parse and validate request parameters with Zod
- const parseResult = pageSpeedRequestSchema.safeParse({
- url: searchParams.get("url") || "https://www.coldbydefault.com",
- strategy: searchParams.get("strategy") || "mobile",
- refresh: searchParams.get("refresh") === "true",
- });
-
- if (!parseResult.success) {
- return NextResponse.json(
- {
- error: "Invalid request parameters",
- details: parseResult.error.issues.map((issue) => issue.message),
- },
- { status: 400 }
- );
- }
-
- const { url, strategy, refresh: forceRefresh } = parseResult.data;
-
- // Check cache first
- const cachedEntry = cache.get(url, strategy);
- const isStale = cache.isStale(url, strategy);
-
- const desktopStaleTolerance =
- strategy === "desktop" &&
- cachedEntry &&
- Date.now() - cachedEntry.timestamp < 24 * 60 * 60 * 1000; // 24 hours for desktop
-
- // If we have fresh data, return it immediately
- if (cachedEntry && !isStale && !forceRefresh) {
- return NextResponse.json(cachedEntry.data, {
- headers: {
- "Cache-Control": "public, max-age=43200", // 12 hours browser cache
- "X-Cache": "HIT",
- },
- });
- }
-
- // If we have stale data and not force refreshing, return stale data and trigger background refresh
- // For desktop, should act more aggressive about returning stale data
- if (cachedEntry && (!forceRefresh || desktopStaleTolerance)) {
- // Trigger background refresh (fire and forget) only if not too recent
- if (isStale) {
- backgroundRefresh(url, strategy).catch(console.error);
- }
-
- return NextResponse.json(cachedEntry.data, {
- headers: {
- "Cache-Control": "public, max-age=300", // 5 minutes browser cache for stale data
- "X-Cache": desktopStaleTolerance ? "STALE-DESKTOP" : "STALE",
- },
- });
- }
-
- // Check if we should skip fetching due to circuit breaker
- if (cache.shouldSkipFetch(url, strategy) && cachedEntry) {
- return NextResponse.json(cachedEntry.data, {
- headers: {
- "Cache-Control": "public, max-age=1800", // 30 minutes for circuit breaker
- "X-Cache": "CIRCUIT-BREAKER",
- },
- });
- }
-
- // If no cache or force refresh, fetch fresh data
- try {
- const result = await fetchPageSpeedData(url, strategy);
-
- if (result) {
- cache.set(url, strategy, result);
-
- return NextResponse.json(result, {
- headers: {
- "Cache-Control": "public, max-age=43200", // 12 hours
- "X-Cache": "MISS",
- },
- });
- }
- } catch (error) {
- console.error("Fresh fetch failed:", error);
-
- // Track timeout for circuit breaker
- if (error instanceof Error) {
- const isTimeout =
- error.name === "TimeoutError" ||
- error.name === "AbortError" ||
- error.message.includes("timeout") ||
- error.message.includes("timed out");
-
- if (isTimeout) {
- cache.incrementTimeoutCount(url, strategy);
- }
- }
-
- // If fresh fetch fails but we have stale data, return the stale data
- if (cachedEntry) {
- return NextResponse.json(cachedEntry.data, {
- headers: {
- "Cache-Control": "public, max-age=300",
- "X-Cache": "STALE-ERROR",
- },
- });
- }
-
- // No cache and fetch failed
- if (error instanceof Error) {
- const isTimeout =
- error.name === "TimeoutError" ||
- error.name === "AbortError" ||
- error.message.includes("timeout") ||
- error.message.includes("timed out");
-
- if (isTimeout) {
- // Start background refresh for next time
- backgroundRefresh(url, strategy).catch(console.error);
-
- return NextResponse.json(
- {
- error:
- "PageSpeed analysis timed out. The website may be slow to load. Try refreshing in a few minutes.",
- retryAfter: 300,
- },
- {
- status: 504,
- headers: {
- "Retry-After": "300",
- },
- }
- );
- }
-
- if (error.message.includes("Rate limit")) {
- return NextResponse.json(
- {
- error: "Too many requests. Please wait before trying again.",
- retryAfter: 60,
- },
- {
- status: 429,
- headers: {
- "Retry-After": "60",
- },
- }
- );
- }
-
- if (error.message.includes("not configured")) {
- return NextResponse.json(
- { error: "PageSpeed API key is not configured" },
- { status: 500 }
- );
- }
-
- if (error.message.includes("service unavailable")) {
- return NextResponse.json(
- { error: "Google PageSpeed service is temporarily unavailable" },
- { status: 503 }
- );
- }
- }
-
- return NextResponse.json(
- {
- error: "PageSpeed service is temporarily unavailable",
- details:
- error instanceof Error ? error.message : "Unknown error occurred",
- },
- { status: 503 }
- );
- }
-
- return NextResponse.json(
- { error: "Unable to analyze page speed" },
- { status: 500 }
- );
- } catch (error) {
- console.error("PageSpeed API error:", error);
- return NextResponse.json(
- { error: "Internal server error" },
- { status: 500 }
- );
- }
-}
-
-// Cleanup old cache entries periodically
-setInterval(() => {
- cache.cleanup();
-}, 60 * 60 * 1000); // Every hour
diff --git a/app/page.tsx b/app/page.tsx
index a63d8e8..4e4b655 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,7 +1,7 @@
/**
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
+ */
"use client";
import { Hero } from "@/components/hero";
@@ -22,7 +22,7 @@ const Capabilities = dynamic(
{
loading: () => ,
ssr: false,
- }
+ },
);
const CertificationShowcase = dynamic(
@@ -33,18 +33,7 @@ const CertificationShowcase = dynamic(
{
loading: () => ,
ssr: false,
- }
-);
-
-const PageSpeedInsights = dynamic(
- () =>
- import("@/components/pagespeed").then((mod) => ({
- default: mod.PageSpeedInsights,
- })),
- {
- loading: () => ,
- ssr: false,
- }
+ },
);
const ClientBackground = dynamic(
@@ -55,7 +44,7 @@ const ClientBackground = dynamic(
{
loading: () => null,
ssr: false,
- }
+ },
);
export default function Home() {
@@ -155,27 +144,6 @@ export default function Home() {
-
-
-
- }
- >
-
-
-
- Website Performance
-
-
-
-
-
-
}>
diff --git a/components/pagespeed/PageSpeedInsights.tsx b/components/pagespeed/PageSpeedInsights.tsx
deleted file mode 100644
index e45a1be..0000000
--- a/components/pagespeed/PageSpeedInsights.tsx
+++ /dev/null
@@ -1,348 +0,0 @@
-/**
- * @author ColdByDefault
- * @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
-
-"use client";
-
-import { useState, useEffect } from "react";
-import {
- Card,
- CardContent,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import { Skeleton } from "@/components/ui/skeleton";
-import { Progress } from "@/components/ui/progress";
-import { Separator } from "@/components/ui/separator";
-import { SiGooglecloud } from "react-icons/si";
-import { HiDesktopComputer } from "react-icons/hi";
-import { HiDevicePhoneMobile } from "react-icons/hi2";
-import type {
- PageSpeedResult,
- PageSpeedInsightsProps,
-} from "@/types/configs/pagespeed";
-import { usePageSpeedData } from "@/hooks/use-pageSpeed-data";
-
-const getScoreBadgeColor = (score: number): string => {
- if (score >= 90)
- return "bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-800";
- if (score >= 50)
- return "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-900/20 dark:text-amber-400 dark:border-amber-800";
- return "bg-red-100 text-red-800 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800";
-};
-
-const getCacheStatusInfo = (
- status: "fresh" | "updating" | "updated" | null
-) => {
- switch (status) {
- case "fresh":
- return {
- label: "Fresh",
- className:
- "bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-400",
- };
- case "updating":
- return {
- label: "Updating",
- className:
- "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/20 dark:text-amber-400",
- };
- case "updated":
- return {
- label: "Updated",
- className:
- "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400",
- };
- default:
- return null;
- }
-};
-
-const MetricsSkeleton = () => (
-
- {Array.from({ length: 4 }).map((_, i) => (
-
-
-
-
- ))}
-
-);
-
-const LoadingSkeleton = ({
- progress,
- progressLabel,
-}: {
- progress: number;
- progressLabel: string;
-}) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {progressLabel}
- {progress}%
-
-
-
-
-
-
-
-
-
-);
-
-const MetricsDisplay = ({ data }: { data: PageSpeedResult }) => (
-
-
- Performance Metrics
-
-
- {Object.entries(data.metrics).map(([key, score]) => (
-
-
- {key === "bestPractices" ? "Best Practices" : key}
-
-
- {score as number}
-
-
- ))}
-
-
-);
-
-export default function PageSpeedInsights({
- url = "https://www.coldbydefault.com",
- showRefreshButton = true,
- showBothStrategies = true,
-}: PageSpeedInsightsProps) {
- const [activeStrategy, setActiveStrategy] = useState<"mobile" | "desktop">(
- "desktop"
- );
- const [progress, setProgress] = useState(0);
- const [progressLabel, setProgressLabel] = useState("Initializing...");
-
- const {
- mobileData,
- desktopData,
- loading,
- cacheStatus,
- lastUpdated,
- refresh,
- } = usePageSpeedData({ url, showBothStrategies });
-
- // Progress simulation based on loading state
- useEffect(() => {
- if (loading) {
- // Start progress
- const updateProgress = (value: number, label: string) => {
- setProgress(value);
- setProgressLabel(label);
- };
-
- updateProgress(0, "Connecting to PageSpeed API...");
-
- const progressTimer = setTimeout(() => {
- updateProgress(25, "Analyzing website performance...");
-
- const midTimer = setTimeout(() => {
- updateProgress(60, "Processing metrics...");
- }, 800);
-
- return () => clearTimeout(midTimer);
- }, 300);
-
- return () => clearTimeout(progressTimer);
- } else {
- // Complete progress when data is loaded
- const finalizeProgress = () => {
- setProgress(100);
- setProgressLabel("Analysis complete!");
- };
- finalizeProgress();
- return undefined;
- }
- }, [loading]);
-
- const getCurrentData = () => {
- return activeStrategy === "mobile" ? mobileData : desktopData;
- };
-
- const data = getCurrentData();
-
- if (loading) {
- return (
-
- );
- }
-
- // Error case removed - we always show data (mock or real)
- // The hook handles errors silently and shows mock data instead
-
- if (!data) {
- // This should never happen since we always have mock data
- return (
-
- );
- }
-
- const cacheInfo = getCacheStatusInfo(cacheStatus);
-
- return (
-
-
-
-
-
- PageSpeed Insights
-
- Powered by Google
-
-
- {showBothStrategies && (
-
-
-
-
- )}
- {!showBothStrategies && (
-
- {data.strategy === "desktop" ? (
-
- ) : (
-
- )}
-
- {data.strategy}
-
-
- )}
-
- {data.url}
-
-
-
-
-
- {showRefreshButton && (
- <>
-
-
- {cacheInfo && (
-
- Cache Status:
-
- {cacheInfo.label}
-
-
- )}
-
-
- >
- )}
-
-
-
-
-
-
- {/* Green status dot with inline styles as fallback */}
-
-
-
- {lastUpdated ? (
- <>
- API Online • Last updated:{" "}
- {new Date(lastUpdated).toLocaleDateString()} at{" "}
- {new Date(lastUpdated).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- })}
- >
- ) : (
- "Loading..."
- )}
-
-
- {cacheStatus === "updating" && (
-
- 📡 Auto-refreshing in background
-
- )}
-
-
-
-
- );
-}
diff --git a/components/pagespeed/index.ts b/components/pagespeed/index.ts
deleted file mode 100644
index 74d5054..0000000
--- a/components/pagespeed/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @author ColdByDefault
- * @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
-
-export { default as PageSpeedInsights } from "./PageSpeedInsights";
-export { usePageSpeedData } from "../../hooks/use-pageSpeed-data";
diff --git a/data/hubs/portfolio-section.data.ts b/data/hubs/portfolio-section.data.ts
index fac5b3e..6e37492 100644
--- a/data/hubs/portfolio-section.data.ts
+++ b/data/hubs/portfolio-section.data.ts
@@ -1,7 +1,7 @@
/**
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
+ */
import {
Code,
@@ -80,7 +80,7 @@ export const dataNodes: ArchitectureNode[] = [
{
icon: Target,
title: "API Routes Structure",
- subtitle: "RESTful Endpoints + GitHub API + PageSpeed API",
+ subtitle: "RESTful Endpoints + GitHub API",
color: "bg-orange-500/10 text-orange-600",
},
{
@@ -332,46 +332,6 @@ export function CertificationShowcaseMobile({
);
-}`,
- },
- {
- title: "API Data Hook",
- language: "TypeScript",
- code: `// Data fetching with caching and error handling
-export function usePageSpeedData({
- url,
- showBothStrategies = true,
-}: UsePageSpeedDataProps): UsePageSpeedDataReturn {
- const [mobileData, setMobileData] = useState(null);
- const [desktopData, setDesktopData] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [cacheStatus, setCacheStatus] = useState<
- "fresh" | "updating" | "updated" | null
- >(null);
-
- const fetchStrategy = useCallback(async (
- strategy: "mobile" | "desktop",
- forceRefresh = false
- ): Promise => {
- try {
- const queryParams = new URLSearchParams({ url, strategy });
- if (forceRefresh) queryParams.append("refresh", "true");
-
- const response = await fetch(\`/api/pagespeed?\${queryParams}\`);
- const result = await response.json();
-
- if (strategy === "mobile") {
- setMobileData(result);
- } else {
- setDesktopData(result);
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : "An error occurred");
- }
- }, [url]);
-
- return { mobileData, desktopData, loading, error, cacheStatus, refresh };
}`,
},
];
@@ -415,7 +375,6 @@ export const routeStructure = {
"api/blog/*",
"api/chatbot/*",
"api/github/*",
- "api/pagespeed/*",
],
};
@@ -436,7 +395,7 @@ export const componentStructure = {
{
folder: "hooks/",
description: "Global reusable hooks",
- examples: ["use-mobile.ts", "use-language.ts", "use-pageSpeed-data.ts"],
+ examples: ["use-mobile.ts", "use-language.ts"],
},
{
folder: "lib/",
diff --git a/hooks/use-pageSpeed-data.ts b/hooks/use-pageSpeed-data.ts
deleted file mode 100644
index a09cd28..0000000
--- a/hooks/use-pageSpeed-data.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-/**
- * @author ColdByDefault
- * @copyright 2026 ColdByDefault. All Rights Reserved.
- */
-
-"use client";
-
-import { useState, useEffect, useCallback, useRef } from "react";
-import type {
- PageSpeedResult,
- PageSpeedApiResponse,
-} from "@/types/configs/pagespeed";
-
-interface UsePageSpeedDataProps {
- url: string;
- showBothStrategies?: boolean;
-}
-
-interface UsePageSpeedDataReturn {
- mobileData: PageSpeedResult | null;
- desktopData: PageSpeedResult | null;
- loading: boolean;
- error: string | null;
- cacheStatus: "fresh" | "updating" | "updated" | null;
- lastUpdated: string | null;
- refresh: () => void;
-}
-
-// Fallback mock data - shown immediately while real data loads
-const createMockData = (
- strategy: "mobile" | "desktop",
- url: string,
-): PageSpeedResult => ({
- url,
- strategy,
- metrics: {
- performance: strategy === "desktop" ? 91 : 94,
- accessibility: 93,
- bestPractices: 98,
- seo: 100,
- },
-});
-
-export function usePageSpeedData({
- url,
- showBothStrategies = true,
-}: UsePageSpeedDataProps): UsePageSpeedDataReturn {
- // Initialize with mock data immediately - users see data right away
- const [mobileData, setMobileData] = useState(() =>
- createMockData("mobile", url),
- );
- const [desktopData, setDesktopData] = useState(() =>
- createMockData("desktop", url),
- );
- // Start with loading=false since we have mock data
- const [loading] = useState(false);
- // Never expose errors to users - always null
- const [error] = useState(null);
- const [cacheStatus, setCacheStatus] = useState<
- "fresh" | "updating" | "updated" | null
- >("fresh");
- const [lastUpdated, setLastUpdated] = useState(() =>
- new Date().toISOString(),
- );
-
- // Track if we've fetched real data
- const hasRealData = useRef({ mobile: false, desktop: false });
- const isDevModeDisabled = useRef(false); // Track if API is disabled in dev
- const retryTimeoutRef = useRef | null>(null);
- const fetchFnRef = useRef<((forceRefresh?: boolean) => Promise) | null>(
- null,
- );
-
- const fetchStrategy = useCallback(
- async (
- strategy: "mobile" | "desktop",
- forceRefresh = false,
- ): Promise => {
- try {
- const queryParams = new URLSearchParams({
- url,
- strategy,
- });
-
- if (forceRefresh) {
- queryParams.append("refresh", "true");
- }
-
- const response = await fetch(
- `/api/pagespeed?${queryParams.toString()}`,
- {
- headers: { "X-Client-ID": "pagespeed-component" },
- signal: AbortSignal.timeout(50000),
- },
- );
-
- // Extract and simplify cache status
- const xCache = response.headers.get("X-Cache");
- if (xCache) {
- if (xCache === "HIT") setCacheStatus("fresh");
- else if (xCache.includes("STALE")) setCacheStatus("updating");
- else setCacheStatus("updated");
- }
-
- if (!response.ok) {
- // Silently fail - keep showing mock/cached data
- console.warn(
- `PageSpeed API returned ${response.status} for ${strategy}`,
- );
- return false;
- }
-
- const result = (await response.json()) as PageSpeedApiResponse;
-
- // Check if API is disabled (dev mode) or data is invalid
- if (!result?.metrics || (result as any).disabled) {
- // Keep showing mock data in dev mode
- console.warn(
- `PageSpeed API ${(result as any).disabled ? "disabled in dev mode" : "returned invalid data"} for ${strategy}`,
- );
- // Mark as disabled to prevent infinite retries
- if ((result as any).disabled) {
- isDevModeDisabled.current = true;
- // Mark as having "data" (mock) so we don't retry
- hasRealData.current.mobile = true;
- hasRealData.current.desktop = true;
- }
- return false;
- }
-
- // Check if metrics are all zeros (invalid real data)
- const hasValidMetrics = Object.values(result.metrics).some(
- (value) => value > 0,
- );
- if (!hasValidMetrics) {
- console.warn(`PageSpeed returned zero metrics for ${strategy}`);
- return false;
- }
-
- const validatedResult: PageSpeedResult = {
- url: result.url || url,
- strategy: (result.strategy as "mobile" | "desktop") || strategy,
- metrics: result.metrics,
- ...(result.loadingExperience && {
- loadingExperience: result.loadingExperience,
- }),
- };
-
- // Update with real data
- if (strategy === "mobile") {
- setMobileData(validatedResult);
- hasRealData.current.mobile = true;
- } else {
- setDesktopData(validatedResult);
- hasRealData.current.desktop = true;
- }
-
- setLastUpdated(new Date().toISOString());
- setCacheStatus("fresh");
- return true;
- } catch (err) {
- // Silently fail - keep showing mock/cached data
- console.warn(`PageSpeed fetch failed (${strategy}):`, err);
- return false;
- }
- },
- [url],
- );
-
- // Fetch data with silent background retry on failure
- const fetchAllDataWithRetry = useCallback(
- async (forceRefresh = false): Promise => {
- if (!url) return;
-
- // Skip fetching if API is disabled in dev mode (unless force refresh)
- if (isDevModeDisabled.current && !forceRefresh) {
- return;
- }
-
- setCacheStatus("updating");
-
- try {
- const mobileSuccess = await fetchStrategy("mobile", forceRefresh);
- let needsRetry = !mobileSuccess;
-
- if (showBothStrategies) {
- const desktopSuccess = await fetchStrategy("desktop", forceRefresh);
- needsRetry = !mobileSuccess && !desktopSuccess;
- }
-
- // Schedule silent retry if needed (only if we don't have real data yet and API is not disabled)
- if (
- needsRetry &&
- !hasRealData.current.mobile &&
- !hasRealData.current.desktop &&
- !isDevModeDisabled.current
- ) {
- if (retryTimeoutRef.current) {
- clearTimeout(retryTimeoutRef.current);
- }
- retryTimeoutRef.current = setTimeout(() => {
- console.log("Silent retry: Attempting to fetch PageSpeed data...");
- // Use ref to call the latest version of the function
- void fetchFnRef.current?.(false);
- }, 30000);
- }
- } catch (fetchError) {
- console.warn("Failed to fetch PageSpeed data:", fetchError);
- }
- },
- [url, showBothStrategies, fetchStrategy],
- );
-
- // Keep the ref updated with the latest function
- useEffect(() => {
- fetchFnRef.current = fetchAllDataWithRetry;
- }, [fetchAllDataWithRetry]);
-
- const refresh = useCallback(() => {
- void fetchAllDataWithRetry(true);
- }, [fetchAllDataWithRetry]);
-
- useEffect(() => {
- // Defer fetch to next tick to avoid synchronous setState in effect
- const timeoutId = setTimeout(() => {
- void fetchAllDataWithRetry(false);
- }, 0);
-
- // Cleanup retry timeout on unmount
- return () => {
- clearTimeout(timeoutId);
- if (retryTimeoutRef.current) {
- clearTimeout(retryTimeoutRef.current);
- }
- };
- }, [fetchAllDataWithRetry]);
-
- return {
- mobileData,
- desktopData,
- loading,
- error,
- cacheStatus,
- lastUpdated,
- refresh,
- };
-}
diff --git a/types/configs/pagespeed.ts b/types/configs/pagespeed.ts
deleted file mode 100644
index 045bcfe..0000000
--- a/types/configs/pagespeed.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * PageSpeed Interface Types
- * @author ColdByDefault
- * @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
-
-export interface PageSpeedMetrics {
- performance: number;
- accessibility: number;
- bestPractices: number;
- seo: number;
- pwa?: number;
-}
-
-export interface PageSpeedResult {
- url: string;
- strategy: "mobile" | "desktop";
- metrics: PageSpeedMetrics;
- loadingExperience?: {
- metrics: {
- FIRST_CONTENTFUL_PAINT_MS?: { percentile: number };
- FIRST_INPUT_DELAY_MS?: { percentile: number };
- LARGEST_CONTENTFUL_PAINT_MS?: { percentile: number };
- CUMULATIVE_LAYOUT_SHIFT_SCORE?: { percentile: number };
- };
- };
-}
-
-export interface PageSpeedApiResponse {
- error?: string;
- details?: string;
- url?: string;
- strategy?: string;
- metrics?: PageSpeedMetrics;
- loadingExperience?: PageSpeedResult["loadingExperience"];
- retryAfter?: number;
-}
-
-export interface PageSpeedInsightsProps {
- url?: string;
- showRefreshButton?: boolean;
- showBothStrategies?: boolean;
-}
-
-export interface PageSpeedLighthouseCategory {
- id: string;
- title: string;
- score: number | null;
-}
-
-export interface PageSpeedLighthouseResult {
- categories: {
- performance?: PageSpeedLighthouseCategory;
- accessibility?: PageSpeedLighthouseCategory;
- "best-practices"?: PageSpeedLighthouseCategory;
- seo?: PageSpeedLighthouseCategory;
- pwa?: PageSpeedLighthouseCategory;
- };
-}
-
-export interface PageSpeedApiRawResponse {
- id?: string;
- lighthouseResult?: PageSpeedLighthouseResult;
-}
From 28d7a06f3891ca0239e47b06c7e8090dca0c5e05 Mon Sep 17 00:00:00 2001
From: ColdByDefault
Date: Mon, 16 Feb 2026 14:48:49 +0100
Subject: [PATCH 4/6] Remove unused assets and backup files related to
PortoCard and Technologies components
---
README.md | 2 -
components/contact/ContactSheet.tsx | 10 +-
components/footer/Footer.tsx | 13 +-
package-lock.json | 226 +++++++--------
package.json | 8 +-
.../aboutPorto/PortoCardComponents.tsx.backup | 133 ---------
temp-not-in-use/aboutPorto/index.ts.backup | 9 -
.../aboutPorto/portoCard.tsx.backup | 194 -------------
.../aboutPorto/portoCard.utils.ts.backup | 257 ------------------
temp-not-in-use/assets/cer2.png | Bin 116845 -> 0 bytes
temp-not-in-use/assets/githubC.png | Bin 97506 -> 0 bytes
temp-not-in-use/assets/htmlC.png | Bin 120473 -> 0 bytes
temp-not-in-use/assets/nodecer.jpg | Bin 227185 -> 0 bytes
.../tech/Technologies.logic.ts.backup | 85 ------
temp-not-in-use/tech/Technologies.tsx.backup | 213 ---------------
temp-not-in-use/tech/Technologies.tsx.backup2 | 120 --------
temp-not-in-use/tech/index.ts.backup | 8 -
temp-not-in-use/tech/tech.ts.backup | 244 -----------------
temp-not-in-use/tech/translations.backup.json | 117 --------
.../tech/use-responsive-carousel.ts.backup | 52 ----
20 files changed, 109 insertions(+), 1582 deletions(-)
delete mode 100644 temp-not-in-use/aboutPorto/PortoCardComponents.tsx.backup
delete mode 100644 temp-not-in-use/aboutPorto/index.ts.backup
delete mode 100644 temp-not-in-use/aboutPorto/portoCard.tsx.backup
delete mode 100644 temp-not-in-use/aboutPorto/portoCard.utils.ts.backup
delete mode 100644 temp-not-in-use/assets/cer2.png
delete mode 100644 temp-not-in-use/assets/githubC.png
delete mode 100644 temp-not-in-use/assets/htmlC.png
delete mode 100644 temp-not-in-use/assets/nodecer.jpg
delete mode 100644 temp-not-in-use/tech/Technologies.logic.ts.backup
delete mode 100644 temp-not-in-use/tech/Technologies.tsx.backup
delete mode 100644 temp-not-in-use/tech/Technologies.tsx.backup2
delete mode 100644 temp-not-in-use/tech/index.ts.backup
delete mode 100644 temp-not-in-use/tech/tech.ts.backup
delete mode 100644 temp-not-in-use/tech/translations.backup.json
delete mode 100644 temp-not-in-use/tech/use-responsive-carousel.ts.backup
diff --git a/README.md b/README.md
index 3feeb0e..8f87564 100644
--- a/README.md
+++ b/README.md
@@ -345,8 +345,6 @@ Refer to `LICENSE` & `COPYRIGHT` files for formal wording.
---
## 16. Contact
-Services: service@yazan-abo-ayash.de
-Support: support@yazan-abo-ayash.de
Portfolio: https://www.coldbydefault.com
Documentation: https://docs.coldbydefault.com/
For professional or security inquiries, reach out via the official channels listed above.
diff --git a/components/contact/ContactSheet.tsx b/components/contact/ContactSheet.tsx
index 56443bc..a5f155e 100644
--- a/components/contact/ContactSheet.tsx
+++ b/components/contact/ContactSheet.tsx
@@ -17,7 +17,7 @@ import {
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
-import { MessageSquare, Mails } from "lucide-react";
+import { MessageSquare } from "lucide-react";
import {
FaGithub,
FaLinkedin,
@@ -93,14 +93,6 @@ export default function ContactSheet({
/>
- {/* Email Section */}
-
-
-
- service@yazan-abo-ayash.de
-
-
-
{/* Social Media Section */}
diff --git a/components/footer/Footer.tsx b/components/footer/Footer.tsx
index 0fa6224..0edf8eb 100644
--- a/components/footer/Footer.tsx
+++ b/components/footer/Footer.tsx
@@ -1,17 +1,15 @@
/**
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
+ */
import { Links } from "@/components/footer";
import {
legalLinks,
developerLinks,
- socialLinks,
footerNavLinks,
} from "@/data/main/footerLinks";
import { SiVercel } from "react-icons/si";
-import { Mails } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
@@ -64,15 +62,8 @@ export default function Footer() {