Reduce memory use by caching API calls and using slug endpoints.

Use React cache() for list/slug fetches, hit single-item portfolio/blog routes instead of loading full lists, and dedupe metadata + page requests on news/blog detail pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-14 11:15:51 +03:30
co-authored by Cursor
parent 9455ebe7cd
commit f341b010c0
3 changed files with 79 additions and 29 deletions
+10 -9
View File
@@ -4,17 +4,20 @@ import { notFound } from "next/navigation";
import { NewsArticleDetail } from "@/components/news/NewsArticleDetail"; import { NewsArticleDetail } from "@/components/news/NewsArticleDetail";
import { SiteFooter } from "@/components/SiteFooter"; import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader"; import { SiteHeader } from "@/components/SiteHeader";
import { getBlogs, toDisplayNewsItem } from "@/lib/api"; import { getBlogArticles, getBlogBySlug, toDisplayNewsItem } from "@/lib/api";
export const revalidate = 3600; export const revalidate = 3600;
async function getBlogPosts() { async function getBlogArticleBySlug(slug: string) {
const posts = await getBlogs(); const item = await getBlogBySlug(slug);
return posts.filter((p) => p.type !== "news"); if (!item || item.type === "news") {
return null;
}
return item;
} }
export async function generateStaticParams() { export async function generateStaticParams() {
const posts = await getBlogPosts(); const posts = await getBlogArticles();
return posts.map((item) => ({ slug: item.slug })); return posts.map((item) => ({ slug: item.slug }));
} }
@@ -24,8 +27,7 @@ export async function generateMetadata({
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
}): Promise<Metadata> { }): Promise<Metadata> {
const { slug } = await params; const { slug } = await params;
const posts = await getBlogPosts(); const item = await getBlogArticleBySlug(slug);
const item = posts.find((p) => p.slug === slug);
if (!item) return {}; if (!item) return {};
return { title: item.titleFa || item.title, description: item.abstract || item.titleFa }; return { title: item.titleFa || item.title, description: item.abstract || item.titleFa };
} }
@@ -36,8 +38,7 @@ export default async function BlogDetailPage({
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
}) { }) {
const { slug } = await params; const { slug } = await params;
const posts = await getBlogPosts(); const item = await getBlogArticleBySlug(slug);
const item = posts.find((p) => p.slug === slug);
if (!item) notFound(); if (!item) notFound();
return ( return (
+11 -5
View File
@@ -4,10 +4,18 @@ import { notFound } from "next/navigation";
import { NewsArticleDetail } from "@/components/news/NewsArticleDetail"; import { NewsArticleDetail } from "@/components/news/NewsArticleDetail";
import { SiteFooter } from "@/components/SiteFooter"; import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader"; import { SiteHeader } from "@/components/SiteHeader";
import { getBlogs, toDisplayNewsItem } from "@/lib/api"; import { getBlogBySlug, getBlogs, toDisplayNewsItem } from "@/lib/api";
export const revalidate = 3600; export const revalidate = 3600;
async function getNewsBySlug(slug: string) {
const item = await getBlogBySlug(slug);
if (!item || item.type !== "news") {
return null;
}
return item;
}
export async function generateStaticParams() { export async function generateStaticParams() {
const posts = await getBlogs("news"); const posts = await getBlogs("news");
return posts.map((item) => ({ slug: item.slug })); return posts.map((item) => ({ slug: item.slug }));
@@ -19,8 +27,7 @@ export async function generateMetadata({
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
}): Promise<Metadata> { }): Promise<Metadata> {
const { slug } = await params; const { slug } = await params;
const posts = await getBlogs("news"); const item = await getNewsBySlug(slug);
const item = posts.find((p) => p.slug === slug);
if (!item) return {}; if (!item) return {};
return { title: item.titleFa || item.title, description: item.abstract || item.titleFa }; return { title: item.titleFa || item.title, description: item.abstract || item.titleFa };
} }
@@ -31,8 +38,7 @@ export default async function NewsDetailPage({
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
}) { }) {
const { slug } = await params; const { slug } = await params;
const posts = await getBlogs("news"); const item = await getNewsBySlug(slug);
const item = posts.find((p) => p.slug === slug);
if (!item) notFound(); if (!item) notFound();
return ( return (
+58 -15
View File
@@ -12,6 +12,8 @@
* to projects in their CMS, the filter picks them up with no code change. * to projects in their CMS, the filter picks them up with no code change.
*/ */
import { cache } from "react";
const API_BASE = "https://api.meshkee.com/api/v1"; const API_BASE = "https://api.meshkee.com/api/v1";
const SITE_DOMAIN = "raoofigroup.com"; const SITE_DOMAIN = "raoofigroup.com";
@@ -57,10 +59,9 @@ type PortfoliosResponse = {
pageSize: number; pageSize: number;
}; };
async function apiGet<T>(path: string): Promise<T> { async function apiFetch(path: string): Promise<Response> {
let res: Response;
try { try {
res = await fetch(`${API_BASE}${path}`, { return await fetch(`${API_BASE}${path}`, {
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
next: { revalidate: REVALIDATE_SECONDS }, next: { revalidate: REVALIDATE_SECONDS },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -68,6 +69,24 @@ async function apiGet<T>(path: string): Promise<T> {
} catch (cause) { } catch (cause) {
throw new Error(`meshkee API ${path} timed out or was unreachable`, { cause }); throw new Error(`meshkee API ${path} timed out or was unreachable`, { cause });
} }
}
async function apiGet<T>(path: string): Promise<T> {
const res = await apiFetch(path);
if (!res.ok) {
throw new Error(`meshkee API ${path} responded ${res.status}`);
}
return res.json() as Promise<T>;
}
async function apiGetOptional<T>(path: string): Promise<T | null> {
const res = await apiFetch(path);
if (res.status === 404) {
return null;
}
if (!res.ok) { if (!res.ok) {
throw new Error(`meshkee API ${path} responded ${res.status}`); throw new Error(`meshkee API ${path} responded ${res.status}`);
@@ -77,7 +96,7 @@ async function apiGet<T>(path: string): Promise<T> {
} }
/** All published portfolio items, every page merged, in the CMS's own order. */ /** All published portfolio items, every page merged, in the CMS's own order. */
export async function getPortfolios(): Promise<ApiPortfolioItem[]> { export const getPortfolios = cache(async (): Promise<ApiPortfolioItem[]> => {
const first = await apiGet<PortfoliosResponse>(`/tenants/${SITE_DOMAIN}/portfolios`); const first = await apiGet<PortfoliosResponse>(`/tenants/${SITE_DOMAIN}/portfolios`);
const pageCount = Math.ceil(first.total / first.pageSize); const pageCount = Math.ceil(first.total / first.pageSize);
@@ -91,12 +110,21 @@ export async function getPortfolios(): Promise<ApiPortfolioItem[]> {
.flatMap((page) => page.items) .flatMap((page) => page.items)
.filter((item) => item.status === "published") .filter((item) => item.status === "published")
.sort((a, b) => a.sortOrder - b.sortOrder); .sort((a, b) => a.sortOrder - b.sortOrder);
} });
export async function getPortfolioBySlug(slug: string): Promise<ApiPortfolioItem | null> { /** Single portfolio by slug — avoids loading the full list on detail pages. */
const items = await getPortfolios(); export const getPortfolioBySlug = cache(async (slug: string): Promise<ApiPortfolioItem | null> => {
return items.find((item) => item.slug === slug) ?? null; const data = await apiGetOptional<{ portfolio: ApiPortfolioItem }>(
} `/tenants/${SITE_DOMAIN}/portfolios/${encodeURIComponent(slug)}`,
);
const item = data?.portfolio;
if (!item || item.status !== "published") {
return null;
}
return item;
});
export type PortfolioCategory = { id: string; name: string; count: number }; export type PortfolioCategory = { id: string; name: string; count: number };
@@ -207,7 +235,7 @@ type BlogsResponse = {
}; };
/** All published blog/news items, every page merged. Optionally filtered by `type`. */ /** All published blog/news items, every page merged. Optionally filtered by `type`. */
export async function getBlogs(type?: BlogType): Promise<ApiBlogItem[]> { export const getBlogs = cache(async (type?: BlogType): Promise<ApiBlogItem[]> => {
const query = type ? `?type=${type}` : ""; const query = type ? `?type=${type}` : "";
const first = await apiGet<BlogsResponse>(`/tenants/${SITE_DOMAIN}/blogs${query}`); const first = await apiGet<BlogsResponse>(`/tenants/${SITE_DOMAIN}/blogs${query}`);
const pageCount = Math.ceil(first.total / first.pageSize); const pageCount = Math.ceil(first.total / first.pageSize);
@@ -224,12 +252,27 @@ export async function getBlogs(type?: BlogType): Promise<ApiBlogItem[]> {
.flatMap((page) => page.items) .flatMap((page) => page.items)
.filter((item) => item.status === "published") .filter((item) => item.status === "published")
.sort((a, b) => a.sortOrder - b.sortOrder); .sort((a, b) => a.sortOrder - b.sortOrder);
} });
export async function getBlogBySlug(slug: string): Promise<ApiBlogItem | null> { /** Single blog/news item by slug — avoids loading the full list on detail pages. */
const items = await getBlogs(); export const getBlogBySlug = cache(async (slug: string): Promise<ApiBlogItem | null> => {
return items.find((item) => item.slug === slug) ?? null; const data = await apiGetOptional<{ blog: ApiBlogItem }>(
} `/tenants/${SITE_DOMAIN}/blogs/${encodeURIComponent(slug)}`,
);
const item = data?.blog;
if (!item || item.status !== "published") {
return null;
}
return item;
});
/** Blog articles only (excludes news posts). */
export const getBlogArticles = cache(async (): Promise<ApiBlogItem[]> => {
const posts = await getBlogs();
return posts.filter((item) => item.type !== "news");
});
export type DisplayNewsItem = { export type DisplayNewsItem = {
slug: string; slug: string;