fix: improve project data handling and sitemap generation

Refactor project data parsing to ensure type safety by casting 
the project data as a string. Enhance the sitemap generation 
by fetching data from a dynamic API route, allowing for 
more accurate and up-to-date sitemap entries. Remove unused 
project markdown files to clean up the project structure. 
These changes improve code reliability and maintainability.
This commit is contained in:
2025-02-12 17:18:22 +01:00
parent 28f6bb74b8
commit b93226ad6f
10 changed files with 146 additions and 317 deletions

View File

@@ -43,11 +43,16 @@ const ProjectDetails = () => {
useEffect(() => {
const projectData = searchParams.get("project");
if (projectData) {
setProject(JSON.parse(projectData));
setProject(JSON.parse(projectData as string));
// Remove the project data from the URL without reloading the page
const url = new URL(window.location.href);
url.searchParams.delete("project");
window.history.replaceState({}, "", url.toString());
// @ts-expect-error window is defined
if (typeof window !== "undefined") {
// @ts-expect-error window is defined
const url = new URL(window.location.href);
url.searchParams.delete("project");
// @ts-expect-error window is defined
window.history.replaceState({}, "", url.toString());
}
} else {
// Fetch project data based on slug from URL
const slug = params.slug as string;
@@ -61,8 +66,8 @@ const ProjectDetails = () => {
if (!response.ok) {
throw new Error("Failed to fetch project data");
}
const projectData = await response.json();
setProject(projectData.posts[0]); // Assuming the API returns an array of posts
const projectData = (await response.json()) as { posts: Project[] };
setProject(projectData.posts[0]);
} catch (error) {
console.error("Failed to fetch project data:", error);
}
@@ -80,7 +85,9 @@ const ProjectDetails = () => {
);
}
const featureImageUrl = `/api/fetchImage?url=${encodeURIComponent(project.feature_image)}`;
const featureImageUrl = project.feature_image
? `/api/fetchImage?url=${encodeURIComponent(project.feature_image)}`
: "";
return (
<div
@@ -88,23 +95,22 @@ const ProjectDetails = () => {
>
<Header />
<div className="flex-grow">
{/* Hero Section */}
<div className="flex justify-center md:mt-28 px-4 md:px-0">
<div className="relative w-full max-w-4xl h-0 pb-[56.25%] rounded-2xl overflow-hidden">
{" "}
{/* 16:9 Aspect Ratio */}
<Image
src={featureImageUrl}
alt={project.title}
fill
style={{ objectFit: "cover" }}
className="rounded-2xl"
priority={true}
/>
</div>
<div className="flex justify-center mt-14 md:mt-28 px-4 md:px-0">
{featureImageUrl && (
<div className="relative w-full max-w-4xl h-0 pb-[56.25%] rounded-2xl overflow-hidden">
<Image
src={featureImageUrl}
alt={project.title}
fill
style={{ objectFit: "cover" }}
className="rounded-2xl"
priority={true}
/>
</div>
)}
</div>
<div className="flex items-center justify-center mt-4">
<h1 className="text-4xl md:text-6xl font-bold text-white">
<h1 className="text-4xl md:text-6xl font-bold text-gray-600">
{project.title}
</h1>
</div>

68
app/api/sitemap/route.tsx Normal file
View File

@@ -0,0 +1,68 @@
import { NextResponse } from "next/server";
interface Project {
slug: string;
}
interface ProjectsData {
posts: Project[];
}
interface SitemapRoute {
url: string;
lastModified: string;
}
const baseUrl = "http://localhost:3000";
const generateSitemap = async (): Promise<SitemapRoute[]> => {
try {
// Static pages
const staticRoutes: SitemapRoute[] = [
{ url: `${baseUrl}/`, lastModified: new Date().toISOString() },
{
url: `${baseUrl}/privacy-policy`,
lastModified: new Date().toISOString(),
},
{
url: `${baseUrl}/legal-notice`,
lastModified: new Date().toISOString(),
},
];
// Fetch project data from your API
console.log("Fetching project data from API...");
const response = await fetch(`${baseUrl}/api/fetchAllProjects`);
if (!response.ok) {
throw new Error(
`Failed to fetch projects from Ghost: ${response.statusText}`,
);
}
const projectsData = (await response.json()) as ProjectsData;
console.log("Fetched project data:", projectsData);
// Generate dynamic routes for projects
const projectRoutes: SitemapRoute[] = projectsData.posts.map((project) => ({
url: `${baseUrl}/projects/${project.slug}`,
lastModified: new Date().toISOString(),
}));
return [...staticRoutes, ...projectRoutes];
} catch (error) {
console.error("Error generating sitemap:", error);
throw error;
}
};
export async function GET() {
try {
const sitemap = await generateSitemap();
return NextResponse.json(sitemap);
} catch (error) {
console.error("Failed to generate sitemap:", error);
return NextResponse.json(
{ error: "Failed to generate sitemap" },
{ status: 500 },
);
}
}

View File

@@ -1,29 +0,0 @@
// app/api/stats/route.tsx
import {NextResponse} from "next/server";
const stats = {
views: 0,
projectsViewed: {} as { [key: string]: number },
};
export async function GET() {
return NextResponse.json(stats);
}
export async function POST(request: Request) {
const {type, projectId} = await request.json();
if (type === "page_view") {
stats.views += 1;
}
if (type === "project_view" && projectId) {
if (stats.projectsViewed[projectId]) {
stats.projectsViewed[projectId] += 1;
} else {
stats.projectsViewed[projectId] = 1;
}
}
return NextResponse.json({message: "Stats updated", stats});
}

View File

@@ -14,6 +14,10 @@ interface Project {
meta_description: string;
}
interface ProjectsData {
posts: Project[];
}
export default function Projects() {
const [projects, setProjects] = useState<Project[]>([]);
const [isVisible, setIsVisible] = useState(false);
@@ -25,7 +29,7 @@ export default function Projects() {
if (!response.ok) {
throw new Error("Failed to fetch projects from Ghost");
}
const projectsData = await response.json();
const projectsData = (await response.json()) as ProjectsData;
setProjects(projectsData.posts);
setTimeout(() => {
@@ -52,7 +56,7 @@ export default function Projects() {
<Link
key={project.id}
href={{
pathname: `/Projects/${project.slug}`,
pathname: `/projects/${project.slug}`,
query: { project: JSON.stringify(project) },
}}
className="cursor-pointer"

View File

@@ -1,31 +1,52 @@
import {MetadataRoute} from "next";
import fs from "fs";
import path from "path";
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = "https://dki.one";
interface SitemapRoute {
url: string;
lastModified: string;
changeFrequency?:
| "always"
| "hourly"
| "daily"
| "weekly"
| "monthly"
| "yearly"
| "never";
priority?: number;
}
// Static pages
const staticRoutes = [
{url: `${baseUrl}/`, lastModified: new Date().toISOString()},
{url: `${baseUrl}/privacy-policy`, lastModified: new Date().toISOString()},
];
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = "http://localhost:3000";
// Read project markdown files from the public folder
const projectsDirectory = path.join(process.cwd(), "public/projects");
let projectRoutes: { url: string; lastModified: string; }[] = [];
// Fetch the sitemap data from the dynamic API route
const response = await fetch(`${baseUrl}/api/sitemap`);
if (!response.ok) {
throw new Error(`Failed to fetch sitemap: ${response.statusText}`);
}
const sitemapData = (await response.json()) as SitemapRoute[];
if (fs.existsSync(projectsDirectory)) {
const projectFiles = fs.readdirSync(projectsDirectory).filter(file => file.endsWith(".md"));
return sitemapData.map((route) => {
let changeFrequency: SitemapRoute["changeFrequency"];
let priority: number;
projectRoutes = projectFiles.map((file) => {
const slug = file.replace(".md", "");
return {
url: `${baseUrl}/projects/${slug}`,
lastModified: new Date().toISOString(),
};
});
if (route.url === `${baseUrl}/`) {
changeFrequency = "weekly";
priority = 1.0;
} else if (route.url.startsWith(`${baseUrl}/projects`)) {
changeFrequency = "monthly";
priority = 0.8;
} else if (route.url.startsWith(`${baseUrl}/Blog`)) {
changeFrequency = "weekly";
priority = 0.6;
} else {
changeFrequency = "monthly";
priority = 0.5;
}
return [...staticRoutes, ...projectRoutes];
return {
url: route.url,
lastModified: route.lastModified,
changeFrequency,
priority,
};
});
}

View File

@@ -1,89 +0,0 @@
"use client";
import { useState } from "react";
export default function StatsDashboard() {
const [authenticated, setAuthenticated] = useState(false);
const [password, setPassword] = useState("");
const [stats, setStats] = useState<{
views: number;
projectsViewed: Record<string, number>;
} | null>(null);
const [error, setError] = useState("");
const handleLogin = () => {
// Simple password check. Replace with secure authentication.
if (password === "admin123") {
setAuthenticated(true);
fetchStats();
} else {
setError("Incorrect password.");
}
};
const fetchStats = async () => {
try {
const res = await fetch("/api/stats");
const data = await res.json();
setStats(data);
} catch (err) {
console.error(err);
setError("Failed to fetch stats.");
}
};
if (!authenticated) {
return (
<div className="flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-800">
<div className="p-10 bg-white dark:bg-gray-700 rounded shadow-md">
<h2 className="text-2xl font-bold mb-4 text-gray-800 dark:text-white">
Admin Login
</h2>
{error && <p className="text-red-500">{error}</p>}
<input
type="password"
placeholder="Enter Password"
className="w-full p-2 border rounded mb-4 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={handleLogin}
className="w-full p-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Login
</button>
</div>
</div>
);
}
return (
<div className="p-10 bg-gray-100 dark:bg-gray-800 min-h-screen">
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">
Statistics Dashboard
</h1>
{stats ? (
<div className="mt-6">
<p className="text-lg text-gray-700 dark:text-gray-300">
Total Views: {stats.views}
</p>
<h2 className="text-2xl font-semibold mt-4 text-gray-800 dark:text-white">
Project Views:
</h2>
<ul className="list-disc pl-5 mt-2 text-gray-700 dark:text-gray-300">
{Object.entries(stats.projectsViewed).map(([projectId, count]) => (
<li key={projectId}>
Project ID {projectId}: {count} view{count !== 1 ? "s" : ""}
</li>
))}
</ul>
</div>
) : (
<p className="mt-4 text-gray-700 dark:text-gray-300">
Loading statistics...
</p>
)}
</div>
);
}